KytyPS5/tests/Ngs2SamplerTests.cpp

1463 lines
61 KiB
C++

// Exercise sampler controls and rendering without a host audio device.
#include "libs/ngs2.cpp"
#include <bit>
#include <cstdio>
#include <cstdlib>
#include <numbers>
using namespace Libs::Audio::Ngs2;
namespace {
void Check(bool condition, const char* message) {
if (!condition) {
std::fprintf(stderr, "Ngs2SamplerTests: %s\n", message);
std::abort();
}
}
struct Fixture {
Ngs2Internal system;
Ngs2RackInternal rack {};
Ngs2VoiceInternal voice;
Fixture(uint32_t rate, uint32_t channels = 1, Ngs2RackType type = Ngs2RackType::Sampler) {
system.option.sample_rate = 48000;
system.option.num_grain_samples = 256;
rack.ngs = &system;
rack.type = type;
voice.rack = &rack;
SetCapacity(4);
struct Setup {
Ngs2VoiceParamHeader header {40, 0, 0x10000000};
Ngs2WaveformFormat format;
uint32_t flags = 0, reserved = 0;
} setup;
setup.header.id = type == Ngs2RackType::Sampler ? 0x10000000 : 0x40010000;
setup.format = {0x12, channels, rate};
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&voice), &setup.header) == OK,
"sampler setup failed");
voice.state = Ngs2VoicePlayState::Playing;
}
void SetCapacity(uint32_t capacity) {
if (rack.type == Ngs2RackType::Sampler) {
rack.option.sampler.max_waveform_blocks = capacity;
} else {
rack.option.custom_sampler.max_waveform_blocks = capacity;
rack.option.custom_sampler.custom_rack_option.state_size = sizeof(Ngs2CustomSamplerVoiceState);
}
}
Ngs2SamplerVoiceState State() {
Ngs2SamplerVoiceState state {};
if (rack.type == Ngs2RackType::CustomSampler) {
Ngs2CustomSamplerVoiceState custom {};
Check(Ngs2VoiceGetState(reinterpret_cast<uintptr_t>(&voice), &custom.voice_state,
sizeof(custom)) == OK, "custom sampler state failed");
state.voice_state = custom.voice_state;
state.num_decoded_samples = custom.num_decoded_samples;
state.decoded_data_size = custom.decoded_data_size;
state.waveform_data = custom.waveform_data;
state.user_data = custom.user_data;
} else {
Check(Ngs2VoiceGetState(reinterpret_cast<uintptr_t>(&voice), &state.voice_state,
sizeof(state)) == OK, "sampler state failed");
}
return state;
}
int QueueBlocks(const void* data, const Ngs2WaveformBlock* blocks, uint32_t count,
uint32_t flags) {
struct Blocks {
Ngs2VoiceParamHeader header {32, 0, 0x10000001};
const void* data;
uint32_t flags, count;
const Ngs2WaveformBlock* blocks;
} param {{32, 0, 0x10000001}, data, flags, count, blocks};
param.header.id = rack.type == Ngs2RackType::Sampler ? 0x10000001 : 0x40010001;
return Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&voice), &param.header);
}
void Queue(const int16_t* data, uint32_t frames, uint32_t flags, uint32_t repeats = 0,
uint32_t skip = 0) {
const Ngs2WaveformBlock block {
0, size_t(frames + skip) * voice.channels * sizeof(int16_t), repeats, skip, frames, 0,
123};
Check(QueueBlocks(data, &block, 1, flags) == OK, "sampler queue failed");
}
void Render(uint32_t frames) {
voice.rendered = false;
Ngs2RenderVoice(voice, {}, frames);
}
};
void TestWaveformQueueCapacity() {
constexpr int32_t full = static_cast<int32_t>(0x804a8054u);
constexpr int32_t invalid_count = static_cast<int32_t>(0x804a8053u);
for (auto rack: {Ngs2RackType::Sampler, Ngs2RackType::CustomSampler}) {
for (uint32_t capacity: {1u, 4u, 16u}) {
Fixture f(24000, 1, rack);
f.SetCapacity(capacity);
f.system.option.num_grain_samples = 1;
const int16_t pcm[] = {8192, 16384};
const int16_t replacement[] = {24576, 4096};
std::vector<Ngs2WaveformBlock> blocks(capacity + 1);
for (uint32_t i = 0; i < blocks.size(); ++i) {
blocks[i] = {0, sizeof(pcm), 0, 0, 2, 0, i + 1};
}
Check(f.QueueBlocks(pcm, blocks.data(), capacity, 1) == OK,
"configured waveform capacity was not available");
f.Render(1);
const auto phase = f.voice.sample_phase;
Check(f.QueueBlocks(replacement, blocks.data(), 1, 0) == full,
"full waveform queue accepted another guest buffer");
Check(f.QueueBlocks(replacement, blocks.data(), capacity + 1, 4) == invalid_count,
"oversized waveform request did not reject before CLEAR");
Check(f.voice.blocks.size() == capacity && f.voice.accepts_blocks &&
f.voice.sample_phase == phase && f.voice.decoded_samples == 1 &&
f.voice.decoded_bytes == sizeof(int16_t),
"rejected waveform request changed playback or stream continuity");
for (uint32_t i = 0; i < capacity; ++i) {
Check(f.voice.blocks[i].data == reinterpret_cast<const uint8_t*>(pcm) &&
f.voice.blocks[i].info.user_data == i + 1 &&
f.voice.blocks[i].cursor == (i == 0 ? 1u : 0u),
"rejected waveform request replaced an outstanding block");
}
f.Render(3);
Check(f.voice.samples == std::vector<float> {0.375f, 0.5f,
capacity == 1 ? 0.5f : 0.375f} &&
f.voice.blocks.size() == capacity - 1,
"queue rejection changed partially played source audio");
Check(f.QueueBlocks(replacement, blocks.data(), 1, 1) == OK &&
f.voice.blocks.size() == capacity,
"completed waveform block did not release its queue slot");
const auto progress = f.State();
Check(f.QueueBlocks(replacement, blocks.data(), 1, 5) == OK &&
f.voice.blocks.size() == 1 && f.voice.sample_phase == 0 &&
f.voice.decoded_samples == progress.num_decoded_samples &&
f.voice.decoded_bytes == progress.decoded_data_size,
"CLEAR failed to replace a full queue while preserving progress");
f.Render(2);
Check(f.voice.samples == std::vector<float> {0.75f, 0.4375f},
"CLEAR retained an old source or fractional playback position");
}
}
}
struct PcmRing {
static constexpr uint32_t block_frames = 7, block_count = 64;
Fixture& fixture;
std::array<std::array<int16_t, block_frames * 2>, 4> buffers {};
uint32_t next_block = 0, completed = 0;
void QueueNext(uint32_t slot) {
auto& buffer = buffers[slot];
for (uint32_t i = 0; i < block_frames; ++i) {
buffer[2 * i] = static_cast<int16_t>(100 + (next_block * block_frames + i) * 16);
buffer[2 * i + 1] = -buffer[2 * i];
}
const Ngs2WaveformBlock block {0, sizeof(buffer), 0, 0, block_frames, 0, slot};
++next_block;
Check(fixture.QueueBlocks(buffer.data(), &block, 1, next_block == block_count ? 0 : 1) == OK,
"released ring buffer could not be queued again");
}
};
void KYTY_SYSV_ABI RefillPcmRing(const Ngs2VoiceCallbackInfo* info) {
auto& ring = *reinterpret_cast<PcmRing*>(info->data);
const auto slot = ring.completed++ % ring.buffers.size();
Check(info->flag == 1 && info->attributes == 0 && info->user == slot &&
info->block_data == ring.buffers[slot].data() &&
info->size == sizeof(ring.buffers[slot]),
"ring completion callback released the wrong source buffer");
for (const auto& block: ring.fixture.voice.blocks) {
Check(block.data != reinterpret_cast<const uint8_t*>(ring.buffers[slot].data()),
"completion callback retained the buffer about to be overwritten");
}
if (ring.next_block < PcmRing::block_count) {
ring.QueueNext(static_cast<uint32_t>(slot));
}
}
void TestWaveformRingReuse() {
for (auto rack: {Ngs2RackType::Sampler, Ngs2RackType::CustomSampler}) {
Fixture f(48000, 2, rack);
PcmRing ring {f};
f.voice.callback = reinterpret_cast<uintptr_t>(&RefillPcmRing);
f.voice.callback_data = reinterpret_cast<uintptr_t>(&ring);
f.voice.callback_flags = 1;
for (uint32_t slot = 0; slot < ring.buffers.size(); ++slot) {
ring.QueueNext(slot);
}
constexpr std::array<uint32_t, 6> grains {1, 16, 3, 29, 2, 11};
constexpr auto total_frames = PcmRing::block_frames * PcmRing::block_count;
uint32_t rendered = 0, grain_index = 0;
while (rendered < total_frames) {
const auto grain = std::min(grains[grain_index++ % grains.size()], total_frames - rendered);
f.Render(grain);
for (uint32_t i = 0; i < grain; ++i) {
const float expected = (100 + (rendered + i) * 16) / 32768.0f;
Check(f.voice.samples[i] == expected && f.voice.samples[grain + i] == -expected,
"ring buffer reuse replayed overwritten data or mixed stereo channels");
}
rendered += grain;
const auto decoded = f.State().num_decoded_samples;
Check(f.voice.blocks.size() <= ring.buffers.size() &&
decoded >= rendered && decoded - rendered < 256 &&
f.voice.decoded_bytes == decoded * 4 &&
ring.completed == decoded / PcmRing::block_frames,
"ring playback lost queue capacity or decoded progress");
if (decoded < total_frames) {
const auto slot = (decoded / PcmRing::block_frames) % ring.buffers.size();
Check(f.voice.WaveformData() == reinterpret_cast<const uint8_t*>(
ring.buffers[slot].data() + (decoded % PcmRing::block_frames) * 2),
"ring read address did not track the active source buffer");
}
}
Check(f.voice.blocks.empty() && f.voice.state == Ngs2VoicePlayState::Empty &&
ring.completed == PcmRing::block_count,
"finite ring stream did not release all buffers and finish");
}
}
void TestProgressDrivenWaveformRing() {
const auto source_sample = [](uint64_t frame) {
return static_cast<int16_t>(static_cast<int32_t>((frame * 43) % 32768) - 16384);
};
for (auto rack: {Ngs2RackType::Sampler, Ngs2RackType::CustomSampler}) {
Fixture f(44100, 2, rack);
std::array<std::array<int16_t, 1024 * 2>, 4> buffers {};
uint64_t submitted = 0, decoded_bytes = 0, rendered = 0;
uint32_t slot = 0;
const auto refill = [&](uint32_t frames) {
auto& buffer = buffers[slot];
for (const auto& block: f.voice.blocks) {
Check(block.data != reinterpret_cast<const uint8_t*>(buffer.data()),
"decoded progress allowed the guest to overwrite an outstanding ring buffer");
}
for (uint32_t i = 0; i < frames; ++i) {
buffer[i * 2] = source_sample(submitted + i);
buffer[i * 2 + 1] = -buffer[i * 2];
}
const Ngs2WaveformBlock block {0, frames * 4u, 0, 0, frames, 0, slot};
Check(f.QueueBlocks(buffer.data(), &block, 1, 1) == OK,
"progress-driven ring exhausted its waveform slots");
submitted += frames;
slot = (slot + 1) % buffers.size();
};
refill(1024);
for (uint32_t grain = 0; grain < 128; ++grain) {
f.Render(256);
for (uint32_t i = 0; i < 256; ++i) {
const uint64_t phase = (rendered + i) * 44100;
const auto frame = phase / 48000;
const float fraction = static_cast<float>(phase % 48000) / 48000.0f;
const float expected = std::lerp(static_cast<float>(source_sample(frame)),
static_cast<float>(source_sample(frame + 1)), fraction) /
32768.0f;
Check(std::abs(f.voice.samples[i] - expected) < 0.00001f &&
std::abs(f.voice.samples[256 + i] + expected) < 0.00001f,
"progress-driven ring replayed overwritten music samples");
}
rendered += 256;
const auto state = f.State();
const auto delta = state.decoded_data_size - decoded_bytes;
Check(state.decoded_data_size >= decoded_bytes && delta <= sizeof(buffers[0]) &&
delta % 4 == 0 && state.num_decoded_samples * 4 == state.decoded_data_size &&
state.num_decoded_samples <= submitted,
"published sampler progress does not describe decoded source data");
decoded_bytes = state.decoded_data_size;
if (delta != 0) {
refill(static_cast<uint32_t>(delta / 4));
}
}
Check(decoded_bytes > 28 * sizeof(buffers[0]), "ring test did not exercise repeated buffer reuse");
}
}
void TestStreamingResample() {
Fixture f(44100, 2);
std::vector<int16_t> pcm(441 * 2);
for (int i = 0; i < 441; ++i) {
pcm[i * 2] = static_cast<int16_t>(i * 32);
pcm[i * 2 + 1] = static_cast<int16_t>(-i * 32);
}
f.Queue(pcm.data(), 200, 1);
f.Queue(pcm.data() + 400, 241, 0);
for (uint32_t grain = 0; grain < 3; ++grain) {
f.Render(160);
for (uint32_t i = 0; i < 160; ++i) {
const float expected = std::min((grain * 160 + i) * 44100.0 / 48000.0, 440.0) / 1024.0;
Check(std::abs(f.voice.samples[i] - expected) < 0.00001f,
"resampling discontinuity across grain or block boundary");
Check(std::abs(f.voice.samples[160 + i] + expected) < 0.00001f,
"stereo channels were mixed");
}
}
Check(f.voice.blocks.empty(), "final streaming block did not finish");
Check(f.voice.state == Ngs2VoicePlayState::Empty, "finished voice remained active");
Ngs2SamplerVoiceState state {};
Ngs2VoiceGetState(reinterpret_cast<uintptr_t>(&f.voice), &state.voice_state, sizeof(state));
Check(state.num_decoded_samples == 441 && state.decoded_data_size == pcm.size() * 2,
"streaming progress does not match consumed source data");
}
// One encoded mono block with one nonzero spectral coefficient.
constexpr std::array<uint8_t, 36> ATRAC9_MONO_BLOCK {0, 0, 0x04, 0x20, 0x04, 0xc0, 0, 0, 0x1f, 0xc0};
void TestAtrac9Vibration() {
using namespace Libs::Audio::Ajm;
for (uint8_t index: {7, 6}) {
const int channels = index - 5;
std::array<uint8_t, 4> config {0xfe, static_cast<uint8_t>(0x70 | (index << 1)), 0x03, 0xf0};
const auto original = config;
AjmAt9Decoder decoder(channels, 48000, AjmSampleEncoding::Float, 0);
const auto initialized = decoder.Initialize(config.data(), config.size());
AjmSidebandDecAt9CodecInfo info {};
decoder.WriteCodecInfo(&info, sizeof(info), initialized);
Check(initialized.result == OK && initialized.format.channel_num == channels &&
initialized.format.sampling_frequency == 48000 && info.frame_samples == 256 &&
info.frames_in_super_frame == 4 && info.super_frame_size == 128 &&
config == original,
"ATRAC9 vibration config returned incorrect geometry or changed input");
Atrac9CodecInfo parsed {};
Check(Ngs2GetAtrac9CodecInfo(config, parsed) && parsed.channels == channels &&
parsed.samplingRate == 48000 && parsed.frameSamples == 256 &&
parsed.framesInSuperframe == 4 && parsed.superframeSize == 128,
"NGS2 vibration metadata differs from decoder metadata");
// Enlarge the superframe for the synthetic blocks and compare ordinary mono/dual mono.
config[2] = 0x0b;
Check(decoder.Initialize(config.data(), config.size()).result == OK,
"ATRAC9 fixture initialization failed");
config[1] -= 12;
auto reference = std::unique_ptr<void, decltype(&Atrac9ReleaseHandle)>(
Atrac9GetHandle(), Atrac9ReleaseHandle);
Check(reference != nullptr && Atrac9InitDecoder(reference.get(), config.data()) == 0,
"ATRAC9 reference initialization failed");
std::array<uint8_t, 384> encoded {};
for (int frame = 0; frame < 4; ++frame) {
for (int channel = 0; channel < channels; ++channel) {
auto block = encoded.begin() + (frame * channels + channel) * ATRAC9_MONO_BLOCK.size();
std::copy(ATRAC9_MONO_BLOCK.begin(), ATRAC9_MONO_BLOCK.end(), block);
if (frame != 0) *block |= 0x80;
}
}
std::vector<float> actual(4 * 256 * channels), expected(actual.size());
size_t offset = 0;
for (int frame = 0; frame < 4; ++frame) {
int used = 0;
Check(Atrac9DecodeF32(reference.get(), encoded.data() + offset,
expected.data() + frame * 256 * channels, &used, 0) == 0 &&
used == channels * ATRAC9_MONO_BLOCK.size(),
"ATRAC9 reference decoding failed");
offset += used;
}
for (int pass = 0; pass < 2; ++pass) {
if (pass != 0) decoder.Reset();
const auto decoded = decoder.Decode(encoded.data(), encoded.size(), actual.data(),
actual.size() * sizeof(float), true, nullptr);
Check(decoded.result == OK && decoded.frames == 4 &&
decoded.input_consumed == encoded.size() &&
decoded.output_written == actual.size() * sizeof(float) &&
actual == expected && std::ranges::any_of(actual, [](float sample) {
return std::abs(sample) > 0.00001f;
}),
"ATRAC9 vibration decoding or reset differs from mono/dual mono");
}
{
auto invalid = original;
invalid[0] = 0;
Check(decoder.Initialize(invalid.data(), invalid.size()).result != OK &&
!Ngs2GetAtrac9CodecInfo(invalid, parsed),
"ATRAC9 vibration accepted an invalid header");
invalid = original;
invalid[1] |= 1;
Check(decoder.Initialize(invalid.data(), invalid.size()).result != OK &&
!Ngs2GetAtrac9CodecInfo(invalid, parsed),
"ATRAC9 vibration accepted an unsupported extension");
}
}
}
void TestAtrac9ResampleAcrossFrames() {
const auto make_voice = [] {
auto f = std::make_unique<Fixture>(24000);
f->voice.SetupSampler({NGS2_WAVEFORM_TYPE_ATRAC9, 1, 24000, 0xfe4005f0});
f->voice.state = Ngs2VoicePlayState::Playing;
f->voice.decoded_frame[0] = 0.0f;
f->voice.decoded_frame[1] = 0.5f;
f->voice.frame_samples = 2;
f->voice.next_decoded_frame.resize(f->voice.decoded_frame.size());
f->voice.next_decoded_frame[0] = 1.0f;
f->voice.next_decoded_frame[1] = 1.5f;
f->voice.next_frame_samples = 2;
f->voice.decoded_samples = 4;
f->voice.accepts_blocks = false;
return f;
};
auto whole = make_voice();
whole->Render(8);
const std::vector<float> expected {0.0f, 0.25f, 0.5f, 0.75f,
1.0f, 1.25f, 1.5f, 1.5f};
Check(whole->voice.samples == expected && whole->voice.decoded_samples == 4 &&
whole->voice.state == Ngs2VoicePlayState::Empty,
"24 kHz ATRAC9 interpolation or source progress failed at frame boundary");
auto split = make_voice();
split->Render(3);
const auto first = split->voice.samples;
split->Render(5);
std::vector<float> joined = first;
joined.insert(joined.end(), split->voice.samples.begin(), split->voice.samples.end());
Check(joined == expected && split->voice.decoded_samples == 4,
"ATRAC9 resampling changed across render grains");
auto pitched = make_voice();
struct Pitch {
Ngs2VoiceParamHeader header {16, 0, 0x10000005};
float ratio = 2.0f;
uint32_t reserved = 0;
} pitch;
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&pitched->voice), &pitch.header) == OK,
"ATRAC9 pitch control failed");
pitched->Render(4);
Check(pitched->voice.samples == std::vector<float> {0.0f, 0.5f, 1.0f, 1.5f} &&
pitched->voice.decoded_samples == 4,
"ATRAC9 pitch did not use source-rate phase advancement");
}
void TestAtrac9ResampleAfterStarvation() {
Fixture f(24000);
f.voice.SetupSampler({NGS2_WAVEFORM_TYPE_ATRAC9, 1, 24000, 0xfe4005f0});
f.voice.state = Ngs2VoicePlayState::Playing;
f.voice.accepts_blocks = true;
f.voice.decoded_frame[0] = 0.0f;
f.voice.decoded_frame[1] = 0.5f;
f.voice.frame_samples = 2;
f.voice.decoded_samples = 2;
f.Render(4);
Check(f.voice.samples == std::vector<float> {0.0f, 0.25f, 0.5f, 0.5f} &&
f.voice.decoded_samples == 2,
"ATRAC9 resampling lost the final sample before starvation");
f.Render(2);
Check(!f.voice.has_samples && f.voice.state == Ngs2VoicePlayState::Playing &&
f.voice.decoded_samples == 2,
"starved ATRAC9 stream consumed source samples");
f.voice.next_decoded_frame.resize(f.voice.decoded_frame.size());
f.voice.next_decoded_frame[0] = 1.0f;
f.voice.next_decoded_frame[1] = 1.5f;
f.voice.next_frame_samples = 2;
f.voice.decoded_samples += 2;
f.Render(4);
Check(f.voice.samples == std::vector<float> {1.0f, 1.25f, 1.5f, 1.5f} &&
f.voice.decoded_samples == 4,
"ATRAC9 refill lost source-rate phase");
}
void TestWaveformBlockGeometry() {
const Ngs2WaveformFormat format {NGS2_WAVEFORM_TYPE_ATRAC9, 1, 24000, 0xfe4005f0};
Ngs2WaveformBlock block {};
Check(Ngs2CalcWaveformBlock(&format, 0, 34534, &block) == OK &&
block.data_offset == 0 && block.data_size == 13056 &&
block.num_skip_samples == 0 && block.num_samples == 34534,
"ATRAC9 block calculation omitted encoded superframes");
Check(Ngs2CalcWaveformBlock(&format, 513, 1, &block) == OK &&
block.data_offset == 192 && block.data_size == 192 &&
block.num_skip_samples == 1 && block.num_samples == 1,
"ATRAC9 block calculation lost the source offset");
Check(Ngs2CalcWaveformBlock(&format, 0, 0, &block) == OK && block.data_size == 0 &&
block.num_samples == 0,
"empty ATRAC9 block acquired encoded data");
Fixture f(24000);
const int16_t silence = 0;
f.Queue(&silence, 0, 0, UINT32_MAX);
f.Render(1);
Check(f.voice.blocks.empty() && f.voice.state == Ngs2VoicePlayState::Empty,
"empty repeated block kept the sampler active");
}
void TestSubmixerSetupAndRouting() {
Fixture f(48000, 2);
Ngs2RackInternal submixer_rack {};
submixer_rack.ngs = &f.system;
submixer_rack.type = Ngs2RackType::Submixer;
submixer_rack.option.submixer.max_channels = 8;
Ngs2VoiceInternal submixer;
submixer.rack = &submixer_rack;
submixer.ports.resize(1);
submixer.matrices.resize(1);
Ngs2RackInternal mastering_rack {};
mastering_rack.type = Ngs2RackType::Mastering;
Ngs2VoiceInternal mastering;
mastering.rack = &mastering_rack;
mastering.channels = 2;
mastering.state = Ngs2VoicePlayState::Playing;
submixer.state = Ngs2VoicePlayState::Playing;
submixer.ports[0].dest = &mastering;
submixer.matrices[0] = {1.0f};
f.voice.ports.push_back({&submixer, 0, 1.0f, -1});
const int16_t pcm[] = {8192, -8192, 16384, -16384};
f.Queue(pcm, 2, 0);
const std::vector<Ngs2VoiceInternal*> voices {&f.voice, &submixer, &mastering};
Ngs2RenderVoice(mastering, voices, 1);
Check(!submixer.has_samples && !mastering.has_samples && f.voice.decoded_samples == 0,
"unconfigured zero-channel submixer claimed to produce samples");
Ngs2SubmixerVoiceSetupParam setup {{16, 0, 0x20000000}, 2, 0};
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&submixer), &setup.header) == OK &&
submixer.channels == 2 && submixer.state == Ngs2VoicePlayState::Empty &&
submixer.ports[0].dest == nullptr && submixer.matrices[0].empty(),
"submixer setup did not establish channels and clear old routing");
submixer.state = Ngs2VoicePlayState::Playing;
submixer.ports[0] = {&mastering, 0, 1.0f, 0};
submixer.matrices[0] = {1.0f, 0.0f, 0.0f, 1.0f};
for (float expected: {0.25f, 0.5f}) {
for (auto* voice: voices) {
voice->rendered = false;
}
Ngs2RenderVoice(mastering, voices, 1);
Check(mastering.has_samples && mastering.samples[0] == expected &&
mastering.samples[1] == -expected,
"submixer lost routed samples across render grains");
}
}
void TestPcm16RenderOutput() {
Ngs2Internal system;
system.option = Ngs2DefaultSystemOption();
system.option.num_grain_samples = 4;
Ngs2RackOptionUnion sampler_option {}, mastering_option {};
Ngs2FillDefaultRackOption(0x1000, &sampler_option);
Ngs2FillDefaultRackOption(0x3000, &mastering_option);
sampler_option.common.max_voices = 2;
sampler_option.common.max_ports = 1;
alignas(Ngs2RackInternal) alignas(Ngs2VoiceInternal)
std::byte sampler_storage[sizeof(Ngs2RackInternal) + 2 * sizeof(Ngs2VoiceInternal)];
alignas(Ngs2RackInternal) alignas(Ngs2VoiceInternal)
std::byte mastering_storage[sizeof(Ngs2RackInternal) + sizeof(Ngs2VoiceInternal)];
Ngs2ContextBufferInfo sampler_buffer {sampler_storage, sizeof(sampler_storage)};
Ngs2ContextBufferInfo mastering_buffer {mastering_storage, sizeof(mastering_storage)};
uintptr_t sampler_handle = 0, mastering_handle = 0;
const auto system_handle = reinterpret_cast<uintptr_t>(&system);
Check(Ngs2RackCreate(system_handle, 0x1000, &sampler_option.common, &sampler_buffer,
&sampler_handle) == OK &&
Ngs2RackCreate(system_handle, 0x3000, &mastering_option.common,
&mastering_buffer, &mastering_handle) == OK,
"PCM16 render rack creation failed");
auto* samplers = reinterpret_cast<Ngs2VoiceInternal*>(
reinterpret_cast<Ngs2RackInternal*>(sampler_handle) + 1);
auto& mastering = *reinterpret_cast<Ngs2VoiceInternal*>(
reinterpret_cast<Ngs2RackInternal*>(mastering_handle) + 1);
mastering.channels = 1;
mastering.state = Ngs2VoicePlayState::Playing;
const int16_t source[2][4] = {{16384, -16384, 32767, -32768},
{8192, -8192, 32767, -32768}};
for (uint32_t i = 0; i < 2; ++i) {
samplers[i].SetupSampler({0x12, 1, 48000});
samplers[i].blocks.push_back(
{reinterpret_cast<const uint8_t*>(source[i]), {0, sizeof(source[i]), 0, 0, 4}});
samplers[i].ports[0].dest = &mastering;
samplers[i].SetEvent(1);
}
int16_t output_samples[4] = {};
Ngs2RenderBufferInfo output {output_samples, sizeof(output_samples), 0x12, 1};
Check(Ngs2SystemRender(system_handle, &output, 1) == OK &&
output_samples[0] == 24576 && output_samples[1] == -24576 &&
output_samples[2] == 32767 && output_samples[3] == -32768,
"PCM16 output failed to mix or saturate mastering voices");
Check(Ngs2SystemRender(system_handle, &output, 1) == OK &&
std::ranges::all_of(output_samples, [](int16_t sample) { return sample == 0; }),
"PCM16 output retained samples from the previous render");
Check(Ngs2RackDestroy(mastering_handle, nullptr) == OK &&
Ngs2RackDestroy(sampler_handle, nullptr) == OK,
"PCM16 render rack cleanup failed");
}
void TestPitchLoopAndSkip() {
Fixture f(24000);
const int16_t pcm[] = {-30000, 8192, 16384};
f.Queue(pcm, 2, 0, 1, 1);
struct Pitch {
Ngs2VoiceParamHeader header {16, 0, 0x10000005};
float ratio = 2.0f;
uint32_t reserved = 0;
} pitch;
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&f.voice), &pitch.header);
f.Render(5);
const float expected[] = {0.25f, 0.5f, 0.25f, 0.5f, 0.0f};
for (size_t i = 0; i < 5; ++i) {
Check(std::abs(f.voice.samples[i] - expected[i]) < 0.00001f,
"pitch, repeat count, or skipped samples incorrect");
}
}
void TestPitchClamps() {
Fixture f(48000);
const int16_t pcm[] = {8192, 0, 0, 0, -8192};
f.Queue(pcm, 5, 0);
struct Pitch {
Ngs2VoiceParamHeader header {16, 0, 0x10000005};
float ratio = 5.0f;
uint32_t reserved = 0;
} pitch;
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&f.voice), &pitch.header) == OK,
"out-of-range pitch was rejected");
f.Render(2);
Check(f.voice.samples[0] == 0.25f && f.voice.samples[1] == -0.25f,
"pitch was not clamped to four times playback speed");
}
void TestStarvationAndPause() {
Fixture f(48000);
const int16_t pcm[] = {16384, 8192};
f.Queue(pcm, 2, 1);
f.Render(4);
Check(f.voice.state == Ngs2VoicePlayState::Playing, "stream starvation stopped voice");
Check(f.voice.samples[2] == 0.0f, "starved output was not silent");
f.Queue(pcm, 2, 0);
f.voice.SetEvent(16);
f.Render(2);
Check(!f.voice.has_samples && f.voice.blocks.front().cursor == 0, "paused voice consumed data");
f.voice.SetEvent(32);
f.Render(2);
Check(f.voice.samples[0] == 0.5f && f.voice.samples[1] == 0.25f,
"stream failed to resume after starvation/pause");
}
void TestMonoRateAndRouting() {
Fixture f(22050);
std::vector<int16_t> pcm(441, 16384);
f.Queue(pcm.data(), 441, 0);
Ngs2RackInternal mastering_rack {};
mastering_rack.type = Ngs2RackType::Mastering;
Ngs2VoiceInternal mastering;
mastering.rack = &mastering_rack;
mastering.channels = 2;
mastering.state = Ngs2VoicePlayState::Playing;
f.voice.matrices = {{0.5f, 0.25f}};
f.voice.ports.push_back({&mastering, 0, 1.0f, 0});
for (int i = 0; i < 3; ++i) {
mastering.rendered = false;
f.voice.rendered = false;
Ngs2RenderVoice(mastering, {&f.voice, &mastering}, 320);
Check(mastering.has_samples, "standard sampler did not reach mastering voice");
for (uint32_t j = 0; j < 320; ++j) {
Check(mastering.samples[j] == 0.25f && mastering.samples[320 + j] == 0.125f,
"mono PCM routing matrix was not applied");
}
}
Check(f.voice.decoded_samples == 441 && f.voice.blocks.empty(),
"22.05 kHz effect duration was incorrect");
}
void TestMatrixOutputChannels() {
Fixture f(48000, 2);
Ngs2RackInternal mastering_rack {};
mastering_rack.type = Ngs2RackType::Mastering;
Ngs2VoiceInternal mastering;
mastering.rack = &mastering_rack;
mastering.channels = 8;
mastering.state = Ngs2VoicePlayState::Playing;
f.voice.matrices = {{1.0f, 0.0f, 0.0f, 1.0f}};
f.voice.ports.push_back({&mastering, 0, 1.0f, 0});
const int16_t pcm[] = {8192, -8192};
f.Queue(pcm, 1, 0);
Ngs2RenderVoice(mastering, {&f.voice, &mastering}, 1);
Check(mastering.samples[0] == 0.25f && mastering.samples[1] == -0.25f,
"stereo matrix did not route its output channels");
for (size_t channel = 2; channel < mastering.channels; ++channel) {
Check(mastering.samples[channel] == 0.0f,
"matrix routed audio to an unspecified output channel");
}
}
void TestCustomPcmStillPlays() {
Fixture f(48000, 1, Ngs2RackType::CustomSampler);
const int16_t pcm[] = {8192, -16384};
f.Queue(pcm, 2, 0);
f.Render(4);
Check(f.voice.samples[0] == 0.25f && f.voice.samples[1] == -0.5f &&
f.voice.samples[2] == 0.0f && f.voice.blocks.empty(),
"custom PCM sampler regressed");
}
void TestSamplerReuseResetsRouting() {
for (auto type: {Ngs2RackType::Sampler, Ngs2RackType::CustomSampler}) {
Fixture f(48000, 1, type);
Ngs2RackInternal mastering_rack {};
mastering_rack.type = Ngs2RackType::Mastering;
Ngs2VoiceInternal mastering;
mastering.rack = &mastering_rack;
mastering.channels = 2;
mastering.state = Ngs2VoicePlayState::Playing;
f.voice.ports.push_back({&mastering, 1, 0.25f, 0});
f.voice.matrices = {{0.5f, 0.25f}};
f.voice.SetupSampler({0x12, 2, 48000});
Check(f.voice.ports.size() == 1 && f.voice.ports[0].dest == nullptr &&
f.voice.ports[0].input == 0 && f.voice.ports[0].volume == 1.0f &&
f.voice.ports[0].matrix == -1 && f.voice.matrices.size() == 1 &&
f.voice.matrices[0].empty(),
"sampler setup retained routing from the previous channel layout");
const int16_t pcm[] = {8192, 16384, -8192, -16384};
f.Queue(pcm, 2, 0);
f.Render(1);
Check(!f.voice.has_samples && f.voice.blocks.front().cursor == 0,
"sampler setup started the new waveform without a play event");
f.voice.ports[0].dest = &mastering;
f.voice.SetEvent(1);
f.voice.rendered = false;
Ngs2RenderVoice(mastering, {&f.voice, &mastering}, 2);
Check(mastering.samples == std::vector<float> {0.25f, -0.25f, 0.5f, -0.5f},
"reused stereo sampler did not restore default channel routing");
}
}
void TestDeferredSamplerSetup() {
Fixture f(48000);
struct Setup {
Ngs2VoiceParamHeader header {40, 0, 0x10000000};
Ngs2WaveformFormat format;
uint32_t flags = 0, reserved = 0;
} setup;
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&f.voice), &setup.header) == OK,
"deferred stream setup failed");
Check(f.voice.state == Ngs2VoicePlayState::Empty && f.voice.channels == 0 &&
f.voice.sample_rate == 0 && f.voice.decoder == nullptr,
"deferred stream setup did not clear the sampler");
setup.format = {0x12, 1, 48000};
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&f.voice), &setup.header) == OK,
"sampler setup after deferred stream failed");
const int16_t pcm = 8192;
f.Queue(&pcm, 1, 0);
f.voice.SetEvent(1);
f.Render(1);
Check(f.voice.samples[0] == 0.25f,
"sampler did not play after deferred stream setup");
}
void TestPlayStateBeforeRender() {
Fixture f(48000);
const int16_t pcm[] = {8192, -16384};
f.voice.state = Ngs2VoicePlayState::Empty;
f.Queue(pcm, 2, 0);
Ngs2VoiceEventParam event {{sizeof(Ngs2VoiceEventParam), 0, 6}, 1};
const auto handle = reinterpret_cast<uintptr_t>(&f.voice);
Ngs2VoiceControl(handle, &event.header);
uint32_t flags = 0;
Ngs2VoiceGetStateFlags(handle, &flags);
Check(flags == 1, "play must reserve the voice before rendering starts playback");
Ngs2SamplerVoiceState state {};
Ngs2VoiceGetState(handle, &state.voice_state, sizeof(state));
Check(state.voice_state.state_flags == flags && state.num_decoded_samples == 0,
"state queries disagreed or play consumed samples before render");
f.Render(4);
Check(f.voice.samples[0] == 0.25f && f.voice.samples[1] == -0.5f,
"one-shot effect was dropped before its first render");
}
void TestStatePublication() {
Ngs2Internal system;
system.option = Ngs2DefaultSystemOption();
Ngs2RackOptionUnion option {};
Ngs2FillDefaultRackOption(0x1000, &option);
option.common.max_voices = 1;
alignas(Ngs2RackInternal) alignas(Ngs2VoiceInternal)
std::byte storage[sizeof(Ngs2RackInternal) + sizeof(Ngs2VoiceInternal)];
Ngs2ContextBufferInfo buffer {storage, sizeof(storage)};
uintptr_t rack_handle = 0;
const auto system_handle = reinterpret_cast<uintptr_t>(&system);
Check(Ngs2RackCreate(system_handle, 0x1000, &option.common, &buffer, &rack_handle) == OK,
"state publication rack creation failed");
auto& voice =
*reinterpret_cast<Ngs2VoiceInternal*>(reinterpret_cast<Ngs2RackInternal*>(rack_handle) + 1);
voice.SetupSampler({0x12, 1, 48000});
const auto handle = reinterpret_cast<uintptr_t>(&voice);
auto check_flags = [&](uint32_t expected) {
uint32_t flags = 0;
Ngs2SamplerVoiceState state {};
Ngs2VoiceGetStateFlags(handle, &flags);
Ngs2VoiceGetState(handle, &state.voice_state, sizeof(state));
Check(flags == expected && state.voice_state.state_flags == expected,
"voice state was published at the wrong render boundary");
};
struct Transition {
uint32_t event, before_render, after_render;
};
const Transition transitions[] = {{1, 1, 3}, {16, 3, 5}, {32, 5, 3}, {2, 3, 0},
{1, 1, 3}, {16, 3, 5}, {2, 5, 0}, {1, 1, 3},
{4, 3, 0}, {1, 1, 3}, {8, 3, 0}};
Ngs2RenderBufferInfo output {};
for (const auto& transition: transitions) {
Ngs2VoiceEventParam event {{sizeof(Ngs2VoiceEventParam), 0, 6}, transition.event};
Ngs2VoiceControl(handle, &event.header);
check_flags(transition.before_render);
Ngs2SystemRender(system_handle, &output, 1);
check_flags(transition.after_render);
}
Check(Ngs2RackDestroy(rack_handle, nullptr) == OK, "state publication rack cleanup failed");
}
void TestStopThenPlayBeforeRender() {
Fixture f(48000);
const int16_t pcm[] = {16384, -8192};
f.Queue(pcm, 2, 0);
f.voice.SetEvent(2);
f.voice.SetEvent(1);
Check(Ngs2GetStateFlags(&f.voice) == 1, "stopped voice could not restart");
f.voice.SetEvent(8);
f.voice.SetEvent(1);
Check(Ngs2GetStateFlags(&f.voice) == 1, "kill/play commands lost their ordering");
f.Render(2);
Check(f.voice.samples[0] == 0.5f && f.voice.samples[1] == -0.25f,
"restarted voice produced no audio");
}
void SetLowPass(Fixture& f, float frequency = 1000.0f, uint64_t mask = 0) {
struct Param {
Ngs2VoiceParamHeader header {56, 0, 0x1000000a};
Ngs2SamplerFilterParam filter {0, 1, 1, 0, 1000.0f, 0.70710678f, 1.0f, {}};
} param;
param.filter.frequency = frequency;
param.filter.channel_mask = mask;
f.rack.option.sampler.max_filters = 8;
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&f.voice), &param.header) == OK,
"filter ABI command failed");
}
void TestLowPassResponse() {
// A second-order Butterworth LPF has unity DC gain and -3 dB at cutoff.
for (double frequency: {100.0, 1000.0, 10000.0}) {
Fixture f(48000);
SetLowPass(f);
std::vector<int16_t> pcm(48000);
for (size_t i = 0; i < pcm.size(); ++i) {
pcm[i] =
static_cast<int16_t>(8192 * std::sin((2.0 * std::numbers::pi) * frequency * i / 48000));
}
f.Queue(pcm.data(), static_cast<uint32_t>(pcm.size()), 0);
f.Render(static_cast<uint32_t>(pcm.size()));
double input = 0, output = 0;
for (size_t i = 24000; i < pcm.size(); ++i) {
input += std::pow(pcm[i] / 32768.0, 2);
output += std::pow(f.voice.samples[i], 2);
}
const double ratio = std::sqrt(output / input);
if (frequency == 100) {
Check(std::abs(ratio - 1.0) < 0.002, "low-pass lost bass");
}
if (frequency == 1000) {
Check(std::abs(ratio - std::sqrt(0.5)) < 0.002, "cutoff gain incorrect");
}
if (frequency == 10000) {
Check(ratio < 0.01, "low-pass did not reject high frequencies");
}
}
}
void TestFilterHistoryAndChannels() {
Fixture whole(48000, 2), split(48000, 2);
SetLowPass(whole, 1000, 1);
SetLowPass(split, 1000, 1);
std::vector<int16_t> pcm(256 * 2, 0);
pcm[0] = pcm[1] = 16384;
whole.Queue(pcm.data(), 256, 0);
split.Queue(pcm.data(), 32, 1);
split.Queue(pcm.data() + 64, 224, 0);
whole.Render(256);
for (uint32_t g = 0; g < 8; ++g) {
// Games resend unchanged parameters; this must not reset the delay line.
SetLowPass(split, 1000, 1);
split.Render(32);
for (uint32_t c = 0; c < 2; ++c) {
for (uint32_t i = 0; i < 32; ++i) {
Check(std::abs(split.voice.samples[c * 32 + i] -
whole.voice.samples[c * 256 + g * 32 + i]) < 1e-7,
"filter history changed at a grain/block/control boundary");
}
}
}
Check(whole.voice.samples[0] == 0.5f && whole.voice.samples[1] == 0.0f &&
whole.voice.samples[256] > 0.0f && whole.voice.samples[256] < 0.5f,
"channel mask must bypass selected channels and filter the others");
Fixture stereo(48000, 2);
SetLowPass(stereo);
pcm[1] = 0;
stereo.Queue(pcm.data(), 256, 0);
stereo.Render(256);
for (size_t i = 256; i < 512; ++i) {
Check(stereo.voice.samples[i] == 0.0f, "filter leaked history between channels");
}
stereo.voice.SetupSampler({0x12, 2, 48000});
Check(stereo.voice.filters.empty(), "reused voice retained old filters");
}
void TestFilterUsesOutputRate() {
Fixture f(22050);
SetLowPass(f, 6833.0f);
const auto& filter = f.voice.filters[0];
// Rear filtering operates at the 48 kHz system rate, not the PCM source rate.
const double w = (2.0 * std::numbers::pi) * 6833 / 48000;
const double expected = (1 - std::cos(w)) / (2 * (1 + std::sin(w) / (2 * double(0.70710678f))));
Check(std::abs(filter.b0 - expected) < 1e-10, "rear filter used source sample rate");
SetLowPass(f, 48000);
const int16_t pcm[] = {16384, 16384};
f.Queue(pcm, 2, 0);
f.Render(2);
Check(f.voice.samples[0] == 0.5f && f.voice.samples[1] == 0.5f,
"above-Nyquist cutoff was unstable");
}
void TestFilterControlLayout() {
Fixture f(48000, 2);
f.rack.option.sampler.max_filters = 8;
// A 56-byte control command with nonzero trailing padding.
alignas(8) uint32_t words[] = {56, 0x1000000a, 0, 1, 1, 0, 0,
0, 0x45d58800, 0x3fd55555, 0x3f800000, 0, 0, 0x13f};
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&f.voice),
reinterpret_cast<const Ngs2VoiceParamHeader*>(words));
Check(f.voice.filters.size() == 1 && f.voice.filters[0].enabled,
"FCQ filter command was not recognized");
const double omega = (2.0 * std::numbers::pi) * 6833.0 / 48000.0;
const double q = static_cast<double>(std::bit_cast<float>(0x3fd55555u));
const double expected = (1 - std::cos(omega)) / (2 * (1 + std::sin(omega) / (2 * q)));
Check(std::abs(f.voice.filters[0].b0 - expected) < 1e-10,
"FCQ fields were read at the wrong offsets");
words[6] = 1; // Bypass channel 0 in the low half of the 64-bit mask.
words[7] = 1; // Bypass channel 32 in the high half.
Fixture wide(48000, 33);
wide.rack.option.sampler.max_filters = 8;
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&wide.voice),
reinterpret_cast<const Ngs2VoiceParamHeader*>(words));
const std::vector<int16_t> pcm(33, 16384);
wide.Queue(pcm.data(), 1, 1);
wide.Render(1);
Check(wide.voice.samples[0] == 0.5f && wide.voice.samples[32] == 0.5f &&
std::abs(wide.voice.samples[1] - expected * 0.5) < 1e-7,
"channel mask was not decoded as a 64-bit bypass mask");
words[4] = 0; // OFF ignores unused FCQ values and clears the old delay line.
words[8] = 0x7fc00000;
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&wide.voice),
reinterpret_cast<const Ngs2VoiceParamHeader*>(words));
Check(!wide.voice.filters[0].enabled && wide.voice.filters[0].history.empty() &&
!wide.voice.filters[0].warned,
"filter OFF was treated as an unsupported FCQ configuration");
wide.Queue(pcm.data(), 1, 1);
wide.Render(1);
Check(wide.voice.samples[1] == 0.5f, "filter OFF did not bypass audio");
SetLowPass(wide, 0);
wide.Queue(pcm.data(), 1, 1);
wide.Render(1);
Check(wide.voice.samples[1] == 0.0f, "zero-Hz low-pass leaked source audio");
}
struct LoopCallbacks {
std::vector<std::pair<uint32_t, uint32_t>> events;
bool pause = false;
};
void KYTY_SYSV_ABI RecordLoopCallback(const Ngs2VoiceCallbackInfo* info) {
auto& log = *reinterpret_cast<LoopCallbacks*>(info->data);
log.events.emplace_back(info->flag, info->repeats);
Check(info->user == 123 && info->attributes == 0, "loop callback metadata changed");
if (log.pause && info->flag == 2) {
reinterpret_cast<Ngs2VoiceInternal*>(info->voice)->SetEvent(16);
}
}
void TestLoopCallbacksAndExit() {
const int16_t pcm[] = {8192, 16384, 24576};
Fixture f(48000);
LoopCallbacks log;
f.voice.callback = reinterpret_cast<uintptr_t>(&RecordLoopCallback);
f.voice.callback_data = reinterpret_cast<uintptr_t>(&log);
f.voice.callback_flags = 3;
f.Queue(pcm, 2, 0, 2);
f.Render(6);
Check(log.events == std::vector<std::pair<uint32_t, uint32_t>> {{2, 1}, {2, 2}, {1, 2}},
"loop and completion callbacks lost the completed repeat count");
Check(f.voice.blocks.empty(), "finite loop did not complete");
Fixture active(48000);
log.events.clear();
log.pause = true;
active.voice.callback = reinterpret_cast<uintptr_t>(&RecordLoopCallback);
active.voice.callback_data = reinterpret_cast<uintptr_t>(&log);
active.voice.callback_flags = 2;
active.Queue(pcm, 2, 1, UINT32_MAX);
active.Queue(pcm + 2, 1, 0);
active.Render(6);
Check(log.events.size() == 1 && active.voice.decoded_samples == 2 &&
active.voice.blocks.front().cursor == 0 && active.voice.samples[2] == 0,
"loop callback pause consumed another iteration");
Ngs2VoiceParamHeader exit_loop {sizeof(Ngs2VoiceParamHeader), 0, 0x10000004};
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&active.voice), &exit_loop);
active.voice.SetEvent(32);
active.Render(5);
Check(active.voice.samples == std::vector<float> {0.25f, 0.5f, 0.25f, 0.5f, 0.75f} &&
active.voice.blocks.empty() && log.events.size() == 1,
"exiting infinite loop lost its final iteration or ignored callback flags");
Fixture pending(48000);
pending.Queue(pcm, 1, 1);
pending.Queue(pcm + 1, 1, 1, UINT32_MAX);
pending.Queue(pcm + 2, 1, 0);
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&pending.voice), &exit_loop);
pending.Render(3);
Check(pending.voice.samples == std::vector<float> {0.25f, 0.5f, 0.75f} &&
pending.voice.blocks.empty(),
"exiting a pending loop skipped its first iteration");
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&pending.voice), &exit_loop);
}
void TestAtrac9Looping() {
// Four encoded mono frames with one nonzero spectral coefficient, then superframe padding.
// The block skips one frame plus one sample and ends inside the third frame.
std::array<uint8_t, 192> encoded {};
for (size_t i = 0; i < 4; ++i) {
std::copy(ATRAC9_MONO_BLOCK.begin(), ATRAC9_MONO_BLOCK.end(),
encoded.begin() + i * ATRAC9_MONO_BLOCK.size());
if (i != 0) encoded[i * ATRAC9_MONO_BLOCK.size()] |= 0x80;
}
const auto make_voice = [&](uint32_t repeats, LoopCallbacks& log) {
auto f = std::make_unique<Fixture>(24000);
f->voice.SetupSampler({NGS2_WAVEFORM_TYPE_ATRAC9, 1, 24000, 0xfe4005f0});
f->voice.state = Ngs2VoicePlayState::Playing;
f->voice.callback = reinterpret_cast<uintptr_t>(&RecordLoopCallback);
f->voice.callback_data = reinterpret_cast<uintptr_t>(&log);
f->voice.callback_flags = 3;
const Ngs2WaveformBlock block {0, encoded.size(), repeats, 129, 137, 0, 123};
struct Blocks {
Ngs2VoiceParamHeader header {32, 0, 0x10000001};
const uint8_t* data;
uint32_t flags, count;
const Ngs2WaveformBlock* blocks;
} param {{32, 0, 0x10000001}, encoded.data(), 0, 1, &block};
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&f->voice), &param.header) == OK,
"compressed loop queue failed");
return f;
};
LoopCallbacks baseline_log, finite_log, split_log, infinite_log;
auto baseline = make_voice(0, baseline_log);
baseline->Render(274);
const auto reference = baseline->voice.samples;
Check(std::ranges::any_of(reference, [](float sample) { return std::abs(sample) > 0.00001f; }),
"synthetic compressed fixture produced no decoded signal");
auto finite = make_voice(2, finite_log);
finite->Render(822);
Check(finite->voice.state == Ngs2VoicePlayState::Empty &&
finite->voice.decoded_samples == 411 && finite->voice.decoded_bytes == 576 &&
finite_log.events == std::vector<std::pair<uint32_t, uint32_t>> {{2, 1}, {2, 2}, {1, 2}},
"compressed finite loop lost samples, input progress, or completion callbacks");
for (size_t i = 0; i < 3; ++i) {
Check(std::equal(reference.begin(), reference.end() - 1,
finite->voice.samples.begin() + i * 274),
"compressed loop did not restore decoder history and skip samples");
const float tail = i == 2 ? reference.back() : std::lerp(reference[272], reference[0], 0.5f);
Check(finite->voice.samples[i * 274 + 273] == tail,
"compressed loop dropped the final frame or broke boundary interpolation");
}
auto split = make_voice(2, split_log);
split->Render(273);
auto joined = split->voice.samples;
split->Render(549);
joined.insert(joined.end(), split->voice.samples.begin(), split->voice.samples.end());
Check(joined == finite->voice.samples && split_log.events == finite_log.events,
"compressed loop changed across render grains");
auto infinite = make_voice(UINT32_MAX, infinite_log);
infinite->Render(300);
Check(infinite->voice.state == Ngs2VoicePlayState::Playing &&
infinite->voice.blocks.front().info.num_repeats == UINT32_MAX,
"compressed infinite loop did not rewind");
Ngs2VoiceParamHeader exit_loop {sizeof(Ngs2VoiceParamHeader), 0, 0x10000004};
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&infinite->voice), &exit_loop);
infinite->Render(248);
Check(infinite->voice.state == Ngs2VoicePlayState::Empty &&
infinite->voice.decoded_samples == 274 &&
infinite_log.events == std::vector<std::pair<uint32_t, uint32_t>> {{2, 1}, {1, 1}},
"compressed loop exit failed to finish the active iteration");
}
void TestAtrac9WaveformJoining() {
std::array<uint8_t, 192> encoded {};
for (size_t i = 0; i < 4; ++i) {
std::copy(ATRAC9_MONO_BLOCK.begin(), ATRAC9_MONO_BLOCK.end(),
encoded.begin() + i * ATRAC9_MONO_BLOCK.size());
if (i != 0) encoded[i * ATRAC9_MONO_BLOCK.size()] |= 0x80;
}
for (auto rack: {Ngs2RackType::Sampler, Ngs2RackType::CustomSampler}) {
Fixture reference(24000, 1, rack);
reference.voice.SetupSampler({NGS2_WAVEFORM_TYPE_ATRAC9, 1, 24000, 0xfe4005f0});
reference.voice.state = Ngs2VoicePlayState::Playing;
const Ngs2WaveformBlock whole {0, encoded.size(), 0, 0, 512, 0, 123};
Check(reference.QueueBlocks(encoded.data(), &whole, 1, 0) == OK,
"compressed reference block could not be queued");
reference.Render(1024);
for (bool nojoin: {false, true}) {
Fixture f(24000, 1, rack);
f.SetCapacity(1);
f.voice.SetupSampler({NGS2_WAVEFORM_TYPE_ATRAC9, 1, 24000, 0xfe4005f0});
f.voice.state = Ngs2VoicePlayState::Playing;
LoopCallbacks callbacks;
f.voice.callback = reinterpret_cast<uintptr_t>(&RecordLoopCallback);
f.voice.callback_data = reinterpret_cast<uintptr_t>(&callbacks);
f.voice.callback_flags = 1;
const Ngs2WaveformBlock first {0, 36, 0, 0, 512, 0, 123};
const Ngs2WaveformBlock rest {36, encoded.size() - 36, 99, 99, 99, 0, 123};
Check(f.QueueBlocks(encoded.data(), &first, 1, 1) == OK,
"compressed stream start could not be queued");
const uint32_t flags = nojoin ? 0x12 : 2;
if (nojoin) {
Check(f.QueueBlocks(encoded.data(), &rest, 1, flags) ==
static_cast<int32_t>(0x804a8054u) &&
f.voice.blocks.size() == 1 && f.voice.blocks.front().info.data_size == 36 &&
f.voice.accepts_blocks,
"NOJOINT bypassed capacity or changed the full stream");
f.SetCapacity(2);
}
Check(f.QueueBlocks(encoded.data(), &rest, 1, flags) == OK &&
f.voice.blocks.size() == (nojoin ? 2u : 1u),
"consecutive ONLY_DATA blocks did not follow the joining flag");
f.Render(1024);
Check(f.voice.samples == reference.voice.samples && f.voice.decoded_samples == 512 &&
f.voice.decoded_bytes == encoded.size() && f.voice.blocks.empty() &&
callbacks.events.size() == (nojoin ? 2u : 1u),
"compressed joining changed audio, progress, or block completion callbacks");
}
Fixture full(24000, 1, rack);
full.SetCapacity(2);
full.voice.SetupSampler({NGS2_WAVEFORM_TYPE_ATRAC9, 1, 24000, 0xfe4005f0});
const Ngs2WaveformBlock initial[] = {{0, 36, 0, 0, 512}, {36, 36, 0, 0, 512}};
const Ngs2WaveformBlock addition[] = {{72, 36}, {144, 48}};
Check(full.QueueBlocks(encoded.data(), initial, 2, 1) == OK &&
full.QueueBlocks(encoded.data(), addition, 2, 2) ==
static_cast<int32_t>(0x804a8054u) &&
full.voice.blocks.size() == 2 && full.voice.blocks.back().info.data_size == 36 &&
full.voice.accepts_blocks,
"overflow after a tentative join partially extended or closed the stream");
}
}
void TestWaveformReadAddress() {
Fixture f(48000, 2);
f.system.option.num_grain_samples = 1;
const int16_t pcm[] = {-100, -100, 8192, 4096, 16384, 8192};
f.Queue(pcm, 2, 1, 1, 1);
const auto next_data = [&]() {
Ngs2SamplerVoiceState state {};
Ngs2VoiceGetState(reinterpret_cast<uintptr_t>(&f.voice), &state.voice_state, sizeof(state));
return state.waveform_data;
};
Check(next_data() == pcm + 2, "read address ignored skipped source samples");
f.Render(1);
Check(next_data() == pcm + 4, "read address did not advance by a stereo frame");
f.Render(1);
Check(next_data() == pcm + 2, "read address did not wrap at a loop boundary");
f.Render(2);
Check(f.voice.blocks.empty() && next_data() == pcm + 6,
"starved read address did not preserve the consumed waveform end");
f.Queue(pcm + 2, 2, 0);
Check(next_data() == pcm + 2, "refilled read address retained the previous waveform end");
f.Render(2);
Check(next_data() == pcm + 6, "completed read address did not preserve waveform end");
f.voice.SetupSampler({0x12, 2, 48000});
Check(next_data() == nullptr, "setup retained an old waveform address");
}
struct CallbackEvent {
Ngs2VoiceInternal* voice;
uint32_t event;
uint32_t calls = 0;
uint32_t trigger = 1;
};
void KYTY_SYSV_ABI ApplyCallbackEvent(const Ngs2VoiceCallbackInfo* info) {
auto& context = *reinterpret_cast<CallbackEvent*>(info->data);
if (++context.calls == context.trigger) {
context.voice->SetEvent(context.event);
}
}
void TestCallbackStopsConsumption() {
for (auto rack: {Ngs2RackType::Sampler, Ngs2RackType::CustomSampler}) {
for (uint32_t event: {2u, 4u, 8u, 16u}) {
Fixture f(48000, 1, rack);
const int16_t pcm[] = {8192, 16384};
f.Queue(pcm, 1, 1);
f.Queue(pcm + 1, 1, 1);
CallbackEvent context {&f.voice, event};
f.voice.callback = reinterpret_cast<uintptr_t>(&ApplyCallbackEvent);
f.voice.callback_data = reinterpret_cast<uintptr_t>(&context);
f.voice.callback_flags = 1;
f.Render(2);
Check(f.voice.state != Ngs2VoicePlayState::Playing && context.calls == 1,
"completion callback failed to interrupt playback");
Check(f.voice.samples[0] == 0.0f && f.voice.samples[1] == 0.0f &&
f.voice.decoded_samples == 1 && f.voice.blocks.size() == 1 &&
f.voice.blocks.front().cursor == 0,
"callback pause/stop consumed the next queued block");
if (event == 16) {
f.voice.SetEvent(32);
f.Render(2);
Check(f.voice.samples == std::vector<float> {0.25f, 0.5f} &&
f.voice.decoded_samples == 2,
"callback pause failed to preserve queued audio for resume");
}
}
}
}
void TestCallbackPauseDuringLargeStep() {
Fixture f(48000);
const int16_t pcm[] = {8192, 16384, 24576, 4096};
f.Queue(pcm, 1, 1);
f.Queue(pcm + 1, 1, 1);
f.Queue(pcm + 2, 2, 1);
f.voice.sample_step = uint64_t(120000) << 32u;
CallbackEvent context {&f.voice, 16};
f.voice.callback = reinterpret_cast<uintptr_t>(&ApplyCallbackEvent);
f.voice.callback_data = reinterpret_cast<uintptr_t>(&context);
f.voice.callback_flags = 1;
f.Render(2);
Check(context.calls == 1 && f.voice.decoded_samples == 1 && f.voice.sample_phase == 0 &&
!f.voice.has_samples,
"pause advanced across additional blocks in a large resampling step");
f.voice.SetEvent(32);
f.Render(2);
Check(f.voice.samples[0] == 0.25f && std::abs(f.voice.samples[1] - 0.4375f) < 1e-7f &&
context.calls == 3,
"resume lost fractional position or read past the next short block");
}
void TestCallbackPauseDuringLookahead() {
for (auto rack: {Ngs2RackType::Sampler, Ngs2RackType::CustomSampler}) {
Fixture f(24000, 1, rack);
f.system.option.num_grain_samples = 1;
int16_t pcm[] = {8192, 16384};
f.Queue(pcm, 1, 1);
f.Queue(pcm + 1, 1, 0);
CallbackEvent context {&f.voice, 16, 0, 2};
f.voice.callback = reinterpret_cast<uintptr_t>(&ApplyCallbackEvent);
f.voice.callback_data = reinterpret_cast<uintptr_t>(&context);
f.voice.callback_flags = 1;
f.Render(1);
Check(f.voice.samples[0] == 0.25f, "initial source sample was not rendered");
f.Render(2);
Check(!f.voice.has_samples && f.voice.state == Ngs2VoicePlayState::Paused &&
context.calls == 2 && f.State().num_decoded_samples == 2 &&
f.voice.sample_phase == (uint64_t(24000) << 32u),
"lookahead callback pause consumed output or lost decoded progress");
pcm[0] = pcm[1] = -32768;
f.voice.SetEvent(32);
f.Render(3);
Check(f.voice.samples == std::vector<float> {0.375f, 0.5f, 0.5f} &&
f.voice.state == Ngs2VoicePlayState::Empty,
"lookahead pause failed to preserve owned samples or fractional playback position");
}
}
struct CallbackReplacement {
Fixture& fixture;
bool setup;
uint32_t trigger, calls = 0;
const std::array<int16_t, 4> pcm {16384, 8192, 4096, 2048};
};
void KYTY_SYSV_ABI ReplaceCallbackSource(const Ngs2VoiceCallbackInfo* info) {
auto& replacement = *reinterpret_cast<CallbackReplacement*>(info->data);
if (++replacement.calls != replacement.trigger) {
return;
}
auto& f = replacement.fixture;
f.voice.callback_flags = 0;
if (replacement.setup) {
f.voice.SetupSampler({0x12, 1, 48000});
}
f.Queue(replacement.pcm.data(), 2, replacement.setup ? 0 : 4);
f.voice.SetEvent(1);
}
void TestCallbackReplacesSource() {
for (auto rack: {Ngs2RackType::Sampler, Ngs2RackType::CustomSampler}) {
for (bool setup: {false, true}) {
for (uint32_t trigger: {1u, 2u}) {
Fixture f(24000, 2, rack);
f.system.option.num_grain_samples = 1;
CallbackReplacement replacement {f, setup, trigger};
const int16_t pcm[] = {8192, 4096};
for (uint32_t i = 0; i < trigger; ++i) {
f.Queue(pcm, 1, 1);
}
f.voice.callback = reinterpret_cast<uintptr_t>(&ReplaceCallbackSource);
f.voice.callback_data = reinterpret_cast<uintptr_t>(&replacement);
f.voice.callback_flags = 1;
f.Render(4);
Check(!f.voice.has_samples && f.voice.samples.size() == (setup ? 4u : 8u) &&
std::ranges::all_of(f.voice.samples, [](float value) { return value == 0; }) &&
f.voice.blocks.size() == 1 && f.voice.blocks.front().cursor == 0,
"callback source replacement used an old frame or channel layout");
f.Render(4);
const std::vector<float> expected = setup
? std::vector<float> {0.5f, 0.25f, 0, 0}
: std::vector<float> {0.5f, 0.3125f, 0.125f, 0.125f,
0.25f, 0.15625f, 0.0625f, 0.0625f};
Check(f.voice.samples == expected && f.voice.state == Ngs2VoicePlayState::Empty,
"callback replacement failed to play the new source on the next render");
}
}
}
}
void TestUpstreamCustomSamplerControls() {
Fixture f(24000, 1, Ngs2RackType::CustomSampler);
const int16_t pcm[] = {8192, 16384, 24576, 4096};
f.Queue(pcm, 4, 1);
f.Render(1);
Check(f.voice.samples[0] == 0.25f, "custom sampler source rate was rejected");
f.Queue(pcm + 2, 2, 4);
f.Render(1);
Check(f.voice.samples[0] == 0.75f && f.voice.blocks.empty() && f.voice.frame_samples == 2,
"replacement blocks retained old data or fractional position");
struct Pitch {
Ngs2VoiceParamHeader header {16, 0, 0x40010005};
float ratio = 0.0f;
uint32_t reserved = 0;
} pitch;
const auto handle = reinterpret_cast<uintptr_t>(&f.voice);
Ngs2VoiceControl(handle, &pitch.header);
f.Render(2);
Check(f.voice.samples[0] == 0.4375f && f.voice.samples[1] == 0.4375f,
"zero custom sampler pitch advanced playback");
pitch.ratio = 4.0f;
Ngs2VoiceControl(handle, &pitch.header);
f.Render(1);
Check(f.voice.blocks.empty(), "custom sampler pitch did not advance playback");
f.rack.option.custom_sampler.custom_rack_option.state_size =
sizeof(Ngs2CustomSamplerVoiceState);
Ngs2CustomSamplerVoiceState state {};
Check(Ngs2VoiceGetState(handle, &state.voice_state, sizeof(state)) == OK &&
state.voice_state.state_flags == Ngs2GetStateFlags(&f.voice),
"custom sampler state layout regressed");
}
void TestFilterTailDuringStarvation() {
Fixture reference(48000), stream(48000);
SetLowPass(reference);
SetLowPass(stream);
std::vector<int16_t> pcm(128, 0);
pcm[0] = pcm[64] = 16384;
reference.Queue(pcm.data(), 128, 1);
reference.Render(128);
stream.Queue(pcm.data(), 1, 1);
stream.Render(1);
Check(stream.voice.samples[0] == reference.voice.samples[0], "initial filter sample differs");
stream.Render(63);
Check(stream.voice.has_samples, "starved filter tail was not available for routing");
for (size_t i = 0; i < 63; ++i) {
Check(std::abs(stream.voice.samples[i] - reference.voice.samples[i + 1]) < 1e-7f,
"filter tail changed at a starvation/grain boundary");
}
stream.Queue(pcm.data() + 64, 64, 1);
stream.Render(64);
for (size_t i = 0; i < 64; ++i) {
Check(std::abs(stream.voice.samples[i] - reference.voice.samples[i + 64]) < 1e-7f,
"refilled stream reused stale filter history");
}
stream.Render(8192);
stream.Render(32);
Check(!stream.voice.has_samples && !stream.voice.filters[0].HasHistory(),
"fully decayed filter kept a silent stream active");
}
void TestResamplingPhaseAfterStarvation() {
for (auto rack: {Ngs2RackType::Sampler, Ngs2RackType::CustomSampler}) {
for (uint32_t rate: {44100u, 96000u}) {
Fixture reference(rate, 1, rack), stream(rate, 1, rack);
const int16_t pcm[] = {0, 0, 16384, 8192};
const uint32_t frames = rate == 44100 ? 2 : 1;
reference.Queue(pcm, 4, 0);
stream.Queue(pcm, 1, 1);
reference.Render(frames);
stream.Render(frames);
Check(stream.voice.blocks.empty() && stream.voice.state == Ngs2VoicePlayState::Playing,
"open stream did not enter starvation");
stream.Render(8);
Check(!stream.voice.has_samples, "starved stream produced audio without a filter");
stream.Queue(pcm + 1, 3, 0);
reference.Render(1);
stream.Render(1);
Check(std::abs(stream.voice.samples[0] - reference.voice.samples[0]) < 1e-7f,
"stream refill lost resampling phase");
Check(stream.voice.decoded_samples == reference.voice.decoded_samples,
"stream refill lost pending source advancement");
stream.voice.SetupSampler({0x12, 1, rate});
Check(stream.voice.sample_phase == 0, "sampler setup retained old resampling phase");
}
}
}
void TestFiniteFilterTail() {
Fixture whole(48000), split(48000);
SetLowPass(whole);
SetLowPass(split);
const int16_t impulse = 16384;
whole.Queue(&impulse, 1, 0);
split.Queue(&impulse, 1, 0);
whole.Render(256);
for (size_t i = 0; i < whole.voice.samples.size(); ++i) {
split.Render(1);
Check(std::abs(split.voice.samples[0] - whole.voice.samples[i]) < 1e-7f,
"finite filter tail changed with render grain size");
}
Check(split.voice.state == Ngs2VoicePlayState::Playing,
"finite voice stopped before its filter tail decayed");
split.Render(8192);
Check(split.voice.state == Ngs2VoicePlayState::Empty,
"finite voice remained active after its filter tail decayed");
split.Render(32);
Check(!split.voice.has_samples, "completed finite filter tail still produced samples");
}
} // namespace
int main() {
TestProgressDrivenWaveformRing();
TestWaveformQueueCapacity();
TestWaveformRingReuse();
TestAtrac9Vibration();
TestPcm16RenderOutput();
TestSubmixerSetupAndRouting();
TestWaveformBlockGeometry();
TestAtrac9ResampleAfterStarvation();
TestAtrac9ResampleAcrossFrames();
TestResamplingPhaseAfterStarvation();
TestSamplerReuseResetsRouting();
TestDeferredSamplerSetup();
TestLoopCallbacksAndExit();
TestAtrac9Looping();
TestAtrac9WaveformJoining();
TestWaveformReadAddress();
TestFilterTailDuringStarvation();
TestFiniteFilterTail();
TestLowPassResponse();
TestFilterHistoryAndChannels();
TestFilterUsesOutputRate();
TestFilterControlLayout();
TestUpstreamCustomSamplerControls();
TestCallbackStopsConsumption();
TestCallbackPauseDuringLargeStep();
TestCallbackPauseDuringLookahead();
TestCallbackReplacesSource();
TestStreamingResample();
TestPitchLoopAndSkip();
TestPitchClamps();
TestStarvationAndPause();
TestMonoRateAndRouting();
TestMatrixOutputChannels();
TestCustomPcmStillPlays();
TestPlayStateBeforeRender();
TestStatePublication();
TestStopThenPlayBeforeRender();
std::puts("Ngs2SamplerTests: all cases passed");
}