// Exercise sampler controls and rendering without a host audio device. #include "libs/ngs2.cpp" #include #include #include #include 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(&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(&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(&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(&voice), ¶m.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(0x804a8054u); constexpr int32_t invalid_count = static_cast(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 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(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 {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 {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, 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(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(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(ring.buffers[slot].data()), "completion callback retained the buffer about to be overwritten"); } if (ring.next_block < PcmRing::block_count) { ring.QueueNext(static_cast(slot)); } } void TestWaveformRingReuse() { for (auto rack: {Ngs2RackType::Sampler, Ngs2RackType::CustomSampler}) { Fixture f(48000, 2, rack); PcmRing ring {f}; f.voice.callback = reinterpret_cast(&RefillPcmRing); f.voice.callback_data = reinterpret_cast(&ring); f.voice.callback_flags = 1; for (uint32_t slot = 0; slot < ring.buffers.size(); ++slot) { ring.QueueNext(slot); } constexpr std::array 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( 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(static_cast((frame * 43) % 32768) - 16384); }; for (auto rack: {Ngs2RackType::Sampler, Ngs2RackType::CustomSampler}) { Fixture f(44100, 2, rack); std::array, 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(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(phase % 48000) / 48000.0f; const float expected = std::lerp(static_cast(source_sample(frame)), static_cast(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(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 pcm(441 * 2); for (int i = 0; i < 441; ++i) { pcm[i * 2] = static_cast(i * 32); pcm[i * 2 + 1] = static_cast(-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(&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 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 config {0xfe, static_cast(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( Atrac9GetHandle(), Atrac9ReleaseHandle); Check(reference != nullptr && Atrac9InitDecoder(reference.get(), config.data()) == 0, "ATRAC9 reference initialization failed"); std::array 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 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(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 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 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(&pitched->voice), &pitch.header) == OK, "ATRAC9 pitch control failed"); pitched->Render(4); Check(pitched->voice.samples == std::vector {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 {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 {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 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(&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(&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( reinterpret_cast(sampler_handle) + 1); auto& mastering = *reinterpret_cast( reinterpret_cast(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(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(&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(&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 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 {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(&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(&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(&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(&system); Check(Ngs2RackCreate(system_handle, 0x1000, &option.common, &buffer, &rack_handle) == OK, "state publication rack creation failed"); auto& voice = *reinterpret_cast(reinterpret_cast(rack_handle) + 1); voice.SetupSampler({0x12, 1, 48000}); const auto handle = reinterpret_cast(&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(&f.voice), ¶m.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 pcm(48000); for (size_t i = 0; i < pcm.size(); ++i) { pcm[i] = static_cast(8192 * std::sin((2.0 * std::numbers::pi) * frequency * i / 48000)); } f.Queue(pcm.data(), static_cast(pcm.size()), 0); f.Render(static_cast(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 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(&f.voice), reinterpret_cast(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(std::bit_cast(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(&wide.voice), reinterpret_cast(words)); const std::vector 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(&wide.voice), reinterpret_cast(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> events; bool pause = false; }; void KYTY_SYSV_ABI RecordLoopCallback(const Ngs2VoiceCallbackInfo* info) { auto& log = *reinterpret_cast(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(info->voice)->SetEvent(16); } } void TestLoopCallbacksAndExit() { const int16_t pcm[] = {8192, 16384, 24576}; Fixture f(48000); LoopCallbacks log; f.voice.callback = reinterpret_cast(&RecordLoopCallback); f.voice.callback_data = reinterpret_cast(&log); f.voice.callback_flags = 3; f.Queue(pcm, 2, 0, 2); f.Render(6); Check(log.events == std::vector> {{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(&RecordLoopCallback); active.voice.callback_data = reinterpret_cast(&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(&active.voice), &exit_loop); active.voice.SetEvent(32); active.Render(5); Check(active.voice.samples == std::vector {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(&pending.voice), &exit_loop); pending.Render(3); Check(pending.voice.samples == std::vector {0.25f, 0.5f, 0.75f} && pending.voice.blocks.empty(), "exiting a pending loop skipped its first iteration"); Ngs2VoiceControl(reinterpret_cast(&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 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(24000); f->voice.SetupSampler({NGS2_WAVEFORM_TYPE_ATRAC9, 1, 24000, 0xfe4005f0}); f->voice.state = Ngs2VoicePlayState::Playing; f->voice.callback = reinterpret_cast(&RecordLoopCallback); f->voice.callback_data = reinterpret_cast(&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(&f->voice), ¶m.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> {{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(&infinite->voice), &exit_loop); infinite->Render(248); Check(infinite->voice.state == Ngs2VoicePlayState::Empty && infinite->voice.decoded_samples == 274 && infinite_log.events == std::vector> {{2, 1}, {1, 1}}, "compressed loop exit failed to finish the active iteration"); } void TestAtrac9WaveformJoining() { std::array 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(&RecordLoopCallback); f.voice.callback_data = reinterpret_cast(&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(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(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(&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(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(&ApplyCallbackEvent); f.voice.callback_data = reinterpret_cast(&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 {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(&ApplyCallbackEvent); f.voice.callback_data = reinterpret_cast(&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(&ApplyCallbackEvent); f.voice.callback_data = reinterpret_cast(&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 {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 pcm {16384, 8192, 4096, 2048}; }; void KYTY_SYSV_ABI ReplaceCallbackSource(const Ngs2VoiceCallbackInfo* info) { auto& replacement = *reinterpret_cast(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(&ReplaceCallbackSource); f.voice.callback_data = reinterpret_cast(&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 expected = setup ? std::vector {0.5f, 0.25f, 0, 0} : std::vector {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(&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 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"); }