From 5b0ef07742f36815e7f759226f87f8555bf07d76 Mon Sep 17 00:00:00 2001 From: Devran Cosmo Uenal Date: Mon, 12 Oct 2026 14:22:06 +0200 Subject: [PATCH] audio: keep NGS2 sampler voices within max_waveform_blocks (#1450) audio: preserve NGS2 streaming buffer ownership --- src/libs/ngs2.cpp | 285 +++++++++++++++----------- tests/Ngs2SamplerTests.cpp | 407 +++++++++++++++++++++++++++++++++++-- 2 files changed, 549 insertions(+), 143 deletions(-) diff --git a/src/libs/ngs2.cpp b/src/libs/ngs2.cpp index b541d0e0..e92cfb9c 100644 --- a/src/libs/ngs2.cpp +++ b/src/libs/ngs2.cpp @@ -30,6 +30,10 @@ LIB_NAME("Ngs2", "Ngs2"); constexpr int32_t NGS2_ERROR_INVALID_OUT_ADDRESS = static_cast(0x804a8010u); +constexpr int32_t NGS2_ERROR_INVALID_NUM_WAVEFORM_BLOCKS = + static_cast(0x804a8053u); +constexpr int32_t NGS2_ERROR_FULL_WAVEFORM_BLOCKS = + static_cast(0x804a8054u); constexpr int32_t NGS2_ERROR_INVALID_WAVEFORM_DATA = static_cast(0x804a8430u); constexpr int32_t NGS2_ERROR_INVALID_WAVEFORM_FORMAT = @@ -549,6 +553,7 @@ struct Ngs2VoiceInternal { uint64_t sample_step = 0; uint64_t decoded_samples = 0; uint64_t decoded_bytes = 0; + uint64_t source_generation = 0; uint32_t duration_remaining = 0; uint32_t skip_remaining = 0; const uint8_t* waveform_end = nullptr; @@ -574,6 +579,19 @@ struct Ngs2VoiceInternal { filters.clear(); } + void ClearWaveformBlocks() { + ++source_generation; + blocks.clear(); + sample_phase = 0; + duration_remaining = 0; + skip_remaining = 0; + compressed_input.clear(); + frame_cursor = 0; + frame_samples = 0; + next_frame_samples = 0; + waveform_end = nullptr; + } + void SetupSampler(const Ngs2WaveformFormat& format) { const bool deferred = format.waveform_type == 0 && format.num_channels == 0 && format.sample_rate == 0 && format.config_data == 0 && @@ -582,23 +600,15 @@ struct Ngs2VoiceInternal { (format.frame_margin != 0 || format.frame_offset != 0 || format.sample_rate == 0 || format.num_channels == 0)); ResetSetupState(); - channels = format.num_channels; - sample_rate = format.sample_rate; - sample_phase = 0; - sample_step = uint64_t(sample_rate) << 32u; - decoded_samples = 0; - decoded_bytes = 0; - duration_remaining = 0; - skip_remaining = 0; - waveform_end = nullptr; - blocks.clear(); + ClearWaveformBlocks(); + channels = format.num_channels; + sample_rate = format.sample_rate; + sample_step = uint64_t(sample_rate) << 32u; + decoded_samples = 0; + decoded_bytes = 0; decoder.reset(); decoded_frame.clear(); next_decoded_frame.clear(); - compressed_input.clear(); - frame_cursor = 0; - frame_samples = 0; - next_frame_samples = 0; accepts_blocks = true; if (deferred) { return; @@ -625,6 +635,63 @@ struct Ngs2VoiceInternal { } } + int AddWaveformBlocks(const uint8_t* data, uint32_t flags, uint32_t count, + const Ngs2WaveformBlock* waveform_blocks) { + const auto capacity = rack->type == Ngs2RackType::Sampler + ? rack->option.sampler.max_waveform_blocks + : rack->option.custom_sampler.max_waveform_blocks; + if (count > capacity) { + return NGS2_ERROR_INVALID_NUM_WAVEFORM_BLOCKS; + } + EXIT_NOT_IMPLEMENTED(!accepts_blocks || (flags & ~0x17u) != 0); + const bool clear = (flags & 4u) != 0; + const bool only_data = (flags & 2u) != 0; + const bool join = only_data && (flags & 0x10u) == 0; + EXIT_NOT_IMPLEMENTED(only_data && decoder == nullptr); + const auto retained = clear ? 0 : blocks.size(); + Block tail = retained == 0 ? Block {} : blocks.back(); + std::vector pending; + pending.reserve(count); + for (uint32_t i = 0; i < count; ++i) { + auto info = waveform_blocks[i]; + if (!only_data && info.num_samples == 0 && info.data_size == 0) { + continue; + } + EXIT_NOT_IMPLEMENTED(!only_data && info.num_samples == 0); + if (only_data) { + info.num_repeats = 0; + info.num_skip_samples = 0; + info.num_samples = 0; + } + EXIT_NOT_IMPLEMENTED(info.num_repeats != 0 && rack->type != Ngs2RackType::Sampler); + EXIT_NOT_IMPLEMENTED(decoder == nullptr && + (uint64_t(info.num_skip_samples) + info.num_samples) * + channels * sizeof(int16_t) > + info.data_size); + Block block {data + info.data_offset, info}; + auto* previous = pending.empty() ? (retained == 0 ? nullptr : &tail) + : &pending.back(); + if (join && previous != nullptr && + previous->data + previous->info.data_size == block.data) { + previous->info.data_size += block.info.data_size; + } else { + pending.push_back(block); + } + } + // Validate the complete addition before clearing, extending, or closing the stream. + if (retained + pending.size() > capacity) { + return NGS2_ERROR_FULL_WAVEFORM_BLOCKS; + } + if (clear) { + ClearWaveformBlocks(); + } else if (retained != 0) { + blocks.back().info.data_size = tail.info.data_size; + } + blocks.insert(blocks.end(), pending.begin(), pending.end()); + accepts_blocks = (flags & 1u) != 0; + return OK; + } + void SetMatrix(uint32_t index, const float* levels, uint32_t count) { EXIT_NOT_IMPLEMENTED(index >= matrices.size()); matrices[index].assign(levels, levels + count); @@ -1425,9 +1492,10 @@ static_assert(offsetof(Ngs2VoiceCallbackInfo, size) == 40 && offsetof(Ngs2VoiceCallbackInfo, repeats) == 44 && offsetof(Ngs2VoiceCallbackInfo, attributes) == 48); -static void Ngs2FinishBlock(Ngs2VoiceInternal& voice) { - auto& block = voice.blocks.front(); - const bool repeat = block.info.num_repeats != 0; +static bool Ngs2FinishBlock(Ngs2VoiceInternal& voice) { + const auto generation = voice.source_generation; + auto& block = voice.blocks.front(); + const bool repeat = block.info.num_repeats != 0; voice.waveform_end = voice.WaveformData(); if (repeat) { if (block.info.num_repeats != UINT32_MAX) { @@ -1458,32 +1526,41 @@ static void Ngs2FinishBlock(Ngs2VoiceInternal& voice) { reinterpret_cast(voice.callback)( &info); } + return voice.source_generation == generation; } -static void Ngs2AdvancePcm(Ngs2VoiceInternal& voice) { - const auto output_rate = uint64_t(voice.rack->ngs->option.sample_rate) << 32u; - // Completion callbacks may interrupt advancement across multiple blocks. +static bool Ngs2DecodePcmFrame(Ngs2VoiceInternal& voice, std::vector& frame, + uint32_t& frame_samples) { + // PCM has no encoded frame size. Use one system grain as a bounded staging + // quantum, keeping source decoding separate from resampled output consumption. + const auto quantum = std::max(voice.rack->ngs->option.num_grain_samples, 1u); + frame.resize(size_t(quantum) * voice.channels); + frame_samples = 0; while (voice.state == Ngs2VoicePlayState::Playing && !voice.blocks.empty() && - voice.sample_phase >= output_rate) { - auto& block = voice.blocks.front(); - const auto available = block.info.num_samples - block.cursor; - const auto advance = std::min(voice.sample_phase / output_rate, - uint64_t(available)); - block.cursor += static_cast(advance); - voice.decoded_samples += advance; - voice.decoded_bytes += advance * voice.channels * sizeof(int16_t); - voice.sample_phase -= advance * output_rate; - if (block.cursor == block.info.num_samples) { - Ngs2FinishBlock(voice); + frame_samples < quantum) { + auto& block = voice.blocks.front(); + const auto count = std::min(quantum - frame_samples, block.info.num_samples - block.cursor); + const auto* pcm = reinterpret_cast(voice.WaveformData()); + for (size_t i = 0; i < size_t(count) * voice.channels; ++i) { + frame[size_t(frame_samples) * voice.channels + i] = pcm[i] / 32768.0f; + } + block.cursor += count; + frame_samples += count; + voice.decoded_samples += count; + voice.decoded_bytes += size_t(count) * voice.channels * sizeof(int16_t); + if (block.cursor == block.info.num_samples && !Ngs2FinishBlock(voice)) { + return false; } } - if (voice.blocks.empty() && !voice.accepts_blocks) { - voice.sample_phase = 0; - } + return frame_samples != 0; } static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector& frame, uint32_t& frame_samples) { + if (voice.decoder == nullptr) { + return Ngs2DecodePcmFrame(voice, frame, frame_samples); + } + frame_samples = 0; while (voice.state == Ngs2VoicePlayState::Playing && !voice.blocks.empty()) { auto& block = voice.blocks.front(); if (block.info.num_samples != 0 && block.data_cursor == 0) { @@ -1491,7 +1568,9 @@ static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector& frame, voice.skip_remaining = block.info.num_skip_samples; } if (voice.duration_remaining == 0) { - Ngs2FinishBlock(voice); + if (!Ngs2FinishBlock(voice)) { + return false; + } continue; } const auto skip = @@ -1512,7 +1591,9 @@ static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector& frame, block.data_cursor += bytes; voice.decoded_bytes += bytes; if (voice.compressed_input.size() < info.next_frame_size) { - Ngs2FinishBlock(voice); + if (!Ngs2FinishBlock(voice)) { + return false; + } continue; } } @@ -1529,7 +1610,9 @@ static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector& frame, block.data + block.info.data_size); voice.decoded_bytes += block.info.data_size - block.data_cursor; block.data_cursor = block.info.data_size; - Ngs2FinishBlock(voice); + if (!Ngs2FinishBlock(voice)) { + return false; + } continue; } EXIT_NOT_IMPLEMENTED(result.result != OK || result.input_consumed == 0); @@ -1547,13 +1630,16 @@ static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector& frame, EXIT_NOT_IMPLEMENTED(decoded_samples > voice.duration_remaining); frame_samples = decoded_samples; voice.duration_remaining -= decoded_samples; + voice.decoded_samples += decoded_samples; } if (voice.duration_remaining == 0 || (block.data_cursor == block.info.data_size && voice.compressed_input.empty())) { if (voice.duration_remaining == 0) { voice.compressed_input.clear(); } - Ngs2FinishBlock(voice); + if (!Ngs2FinishBlock(voice)) { + return false; + } } if (decoded_samples != 0) { return true; @@ -1562,8 +1648,10 @@ static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector& frame, return false; } -static bool Ngs2CurrentAtracFrame(Ngs2VoiceInternal& voice) { +static bool Ngs2CurrentFrame(Ngs2VoiceInternal& voice) { if (voice.frame_cursor == voice.frame_samples) { + voice.frame_cursor = 0; + voice.frame_samples = 0; if (voice.next_frame_samples != 0) { std::swap(voice.decoded_frame, voice.next_decoded_frame); voice.frame_samples = voice.next_frame_samples; @@ -1571,21 +1659,21 @@ static bool Ngs2CurrentAtracFrame(Ngs2VoiceInternal& voice) { } else if (!Ngs2DecodeFrame(voice, voice.decoded_frame, voice.frame_samples)) { return false; } - voice.frame_cursor = 0; } return voice.state == Ngs2VoicePlayState::Playing && voice.frame_cursor < voice.frame_samples; } -static void Ngs2AdvanceAtrac9(Ngs2VoiceInternal& voice) { +static void Ngs2AdvanceSamples(Ngs2VoiceInternal& voice) { const auto output_rate = uint64_t(voice.rack->ngs->option.sample_rate) << 32u; while (voice.state == Ngs2VoicePlayState::Playing && voice.sample_phase >= output_rate) { - if (!Ngs2CurrentAtracFrame(voice)) { + if (!Ngs2CurrentFrame(voice)) { break; } - ++voice.frame_cursor; - ++voice.decoded_samples; - voice.sample_phase -= output_rate; + const auto advance = std::min(voice.sample_phase / output_rate, + uint64_t(voice.frame_samples - voice.frame_cursor)); + voice.frame_cursor += static_cast(advance); + voice.sample_phase -= advance * output_rate; } if (voice.blocks.empty() && voice.frame_cursor == voice.frame_samples && voice.next_frame_samples == 0 && !voice.accepts_blocks) { @@ -1594,42 +1682,14 @@ static void Ngs2AdvanceAtrac9(Ngs2VoiceInternal& voice) { } static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) { - uint32_t output = 0; + const auto generation = voice.source_generation; + uint32_t output = 0; const auto output_rate = uint64_t(voice.rack->ngs->option.sample_rate) << 32u; while (voice.state == Ngs2VoicePlayState::Playing && output < grain && (!voice.blocks.empty() || voice.frame_cursor < voice.frame_samples || voice.next_frame_samples != 0)) { - if (voice.decoder == nullptr) { - Ngs2AdvancePcm(voice); - if (voice.state != Ngs2VoicePlayState::Playing || voice.blocks.empty()) { - break; - } - const auto& block = voice.blocks.front(); - const auto* pcm = reinterpret_cast(voice.WaveformData()); - const auto* next = pcm; - if (block.cursor + 1 < block.info.num_samples) { - next += voice.channels; - } else if (block.info.num_repeats != 0) { - next = reinterpret_cast(block.data) + - block.info.num_skip_samples * voice.channels; - } else if (voice.blocks.size() > 1) { - const auto& next_block = voice.blocks[1]; - next = reinterpret_cast(next_block.data) + - next_block.info.num_skip_samples * voice.channels; - } - const float fraction = static_cast(voice.sample_phase) / - static_cast(output_rate); - for (uint32_t c = 0; c < voice.channels; ++c) { - voice.samples[c * grain + output] = - std::lerp(static_cast(pcm[c]), static_cast(next[c]), fraction) / - 32768.0f; - } - voice.sample_phase += voice.sample_step; - ++output; - continue; - } - Ngs2AdvanceAtrac9(voice); - if (!Ngs2CurrentAtracFrame(voice)) { + Ngs2AdvanceSamples(voice); + if (voice.source_generation != generation || !Ngs2CurrentFrame(voice)) { break; } if (voice.frame_cursor + 1 == voice.frame_samples && voice.sample_phase != 0) { @@ -1639,7 +1699,8 @@ static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) { } Ngs2DecodeFrame(voice, voice.next_decoded_frame, voice.next_frame_samples); } - if (voice.state != Ngs2VoicePlayState::Playing || + if (voice.source_generation != generation || + voice.state != Ngs2VoicePlayState::Playing || voice.frame_cursor >= voice.frame_samples) { break; } @@ -1657,10 +1718,13 @@ static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) { voice.sample_phase += voice.sample_step; ++output; } - if (voice.decoder == nullptr) { - Ngs2AdvancePcm(voice); - } else { - Ngs2AdvanceAtrac9(voice); + if (voice.source_generation == generation) { + Ngs2AdvanceSamples(voice); + } + if (voice.source_generation != generation) { + // A callback replaced the source, possibly changing its channel layout. + voice.samples.assign(size_t(grain) * voice.channels, 0.0f); + output = 0; } voice.has_samples = output != 0; } @@ -2370,41 +2434,10 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH const Ngs2WaveformBlock* blocks; }; const auto& blocks = *reinterpret_cast(param); - EXIT_NOT_IMPLEMENTED(!voice->accepts_blocks || - (blocks.flags & ~0x17u) != 0); - if ((blocks.flags & 0x4u) != 0) { - voice->blocks.clear(); - voice->sample_phase = 0; - voice->duration_remaining = 0; - voice->skip_remaining = 0; - voice->compressed_input.clear(); - voice->frame_cursor = 0; - voice->frame_samples = 0; - voice->next_frame_samples = 0; - voice->waveform_end = nullptr; - } - voice->accepts_blocks = (blocks.flags & 1u) != 0; - const bool only_data = (blocks.flags & 2u) != 0; - EXIT_NOT_IMPLEMENTED(only_data && voice->decoder == nullptr); - for (uint32_t i = 0; i < blocks.count; ++i) { - auto block = blocks.blocks[i]; - if (!only_data && block.num_samples == 0 && block.data_size == 0) { - continue; - } - EXIT_NOT_IMPLEMENTED(!only_data && block.num_samples == 0); - if (only_data) { - block.num_repeats = 0; - block.num_skip_samples = 0; - block.num_samples = 0; - } - EXIT_NOT_IMPLEMENTED(block.num_repeats != 0 && - voice->rack->type != Ngs2RackType::Sampler); - EXIT_NOT_IMPLEMENTED( - voice->decoder == nullptr && - (uint64_t(block.num_skip_samples) + block.num_samples) * - voice->channels * 2 > - block.data_size); - voice->blocks.push_back({blocks.data + block.data_offset, block}); + const auto result = voice->AddWaveformBlocks( + blocks.data, blocks.flags, blocks.count, blocks.blocks); + if (result != OK) { + return result; } break; } @@ -2511,6 +2544,15 @@ int KYTY_SYSV_ABI Ngs2VoiceRunCommands(uintptr_t voice_handle, const void* comma return OK; } +template +static void Ngs2FillSamplerState(const Ngs2VoiceInternal& voice, State& state) { + state.voice_state.state_flags = Ngs2GetStateFlags(&voice); + state.num_decoded_samples = voice.decoded_samples; + state.decoded_data_size = voice.decoded_bytes; + state.waveform_data = voice.WaveformData(); + state.user_data = voice.blocks.empty() ? 0 : voice.blocks.front().info.user_data; +} + int KYTY_SYSV_ABI Ngs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* state, size_t state_size) { PRINT_NAME(); @@ -2547,7 +2589,7 @@ int KYTY_SYSV_ABI Ngs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* stat EXIT_NOT_IMPLEMENTED(state_size != configured_size); std::memset(state, 0, state_size); auto* sampler = reinterpret_cast(state); - sampler->voice_state.state_flags = Ngs2GetStateFlags(voice); + Ngs2FillSamplerState(*voice, *sampler); break; } case Ngs2RackType::Sampler: { @@ -2568,12 +2610,7 @@ int KYTY_SYSV_ABI Ngs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* stat sampler->envelope_height = 1.0f; sampler->peak_height = 0.0f; sampler->reserved = 0; - sampler->num_decoded_samples = voice->decoded_samples; - sampler->decoded_data_size = voice->decoded_bytes; - sampler->waveform_data = voice->WaveformData(); - if (!voice->blocks.empty()) { - sampler->user_data = voice->blocks.front().info.user_data; - } + Ngs2FillSamplerState(*voice, *sampler); break; } default: EXIT("unknown type: %s\n", magic_enum::enum_name(voice->rack->type)); diff --git a/tests/Ngs2SamplerTests.cpp b/tests/Ngs2SamplerTests.cpp index 1d6f222b..13b92cdd 100644 --- a/tests/Ngs2SamplerTests.cpp +++ b/tests/Ngs2SamplerTests.cpp @@ -24,9 +24,11 @@ struct Fixture { 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; @@ -39,20 +41,51 @@ struct Fixture { voice.state = Ngs2VoicePlayState::Playing; } - void Queue(const int16_t* data, uint32_t frames, uint32_t flags, uint32_t repeats = 0, - uint32_t skip = 0) { - Ngs2WaveformBlock block { - 0, size_t(frames + skip) * voice.channels * sizeof(int16_t), repeats, skip, frames, 0, - 123}; + 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, 1, &block}; + } param {{32, 0, 0x10000001}, data, flags, count, blocks}; param.header.id = rack.type == Ngs2RackType::Sampler ? 0x10000001 : 0x40010001; - Check(Ngs2VoiceControl(reinterpret_cast(&voice), ¶m.header) == OK, - "sampler queue failed"); + 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) { @@ -61,6 +94,190 @@ struct Fixture { } }; +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); @@ -178,6 +395,7 @@ void TestAtrac9ResampleAcrossFrames() { 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; }; @@ -218,6 +436,7 @@ void TestAtrac9ResampleAfterStarvation() { 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, @@ -230,6 +449,7 @@ void TestAtrac9ResampleAfterStarvation() { 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, @@ -773,8 +993,8 @@ void TestLoopCallbacksAndExit() { Ngs2VoiceParamHeader exit_loop {sizeof(Ngs2VoiceParamHeader), 0, 0x10000004}; Ngs2VoiceControl(reinterpret_cast(&active.voice), &exit_loop); active.voice.SetEvent(32); - active.Render(3); - Check(active.voice.samples == std::vector {0.25f, 0.5f, 0.75f} && + 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"); @@ -858,8 +1078,69 @@ void TestAtrac9Looping() { "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 = [&]() { @@ -887,11 +1168,12 @@ 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 == 1) { + if (++context.calls == context.trigger) { context.voice->SetEvent(context.event); } } @@ -910,14 +1192,15 @@ void TestCallbackStopsConsumption() { f.Render(2); Check(f.voice.state != Ngs2VoicePlayState::Playing && context.calls == 1, "completion callback failed to interrupt playback"); - Check(f.voice.samples[0] == 0.25f && f.voice.samples[1] == 0.0f && + 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(1); - Check(f.voice.samples[0] == 0.5f && f.voice.decoded_samples == 2, + 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"); } } @@ -936,15 +1219,95 @@ void TestCallbackPauseDuringLargeStep() { 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 == (uint64_t(72000) << 32u), + 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(1); - Check(std::abs(f.voice.samples[0] - 0.4375f) < 1e-7f && context.calls == 3, + 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}; @@ -953,7 +1316,7 @@ void TestUpstreamCustomSamplerControls() { 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.size() == 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}; @@ -1058,6 +1421,9 @@ void TestFiniteFilterTail() { } // namespace int main() { + TestProgressDrivenWaveformRing(); + TestWaveformQueueCapacity(); + TestWaveformRingReuse(); TestAtrac9Vibration(); TestPcm16RenderOutput(); TestSubmixerSetupAndRouting(); @@ -1069,6 +1435,7 @@ int main() { TestDeferredSamplerSetup(); TestLoopCallbacksAndExit(); TestAtrac9Looping(); + TestAtrac9WaveformJoining(); TestWaveformReadAddress(); TestFilterTailDuringStarvation(); TestFiniteFilterTail(); @@ -1079,6 +1446,8 @@ int main() { TestUpstreamCustomSamplerControls(); TestCallbackStopsConsumption(); TestCallbackPauseDuringLargeStep(); + TestCallbackPauseDuringLookahead(); + TestCallbackReplacesSource(); TestStreamingResample(); TestPitchLoopAndSkip(); TestPitchClamps();