diff --git a/src/libs/ngs2.cpp b/src/libs/ngs2.cpp index 03282435d..02d697d48 100644 --- a/src/libs/ngs2.cpp +++ b/src/libs/ngs2.cpp @@ -541,7 +541,6 @@ struct Ngs2VoiceInternal { struct Block { const uint8_t* data; Ngs2WaveformBlock info; - uint32_t cursor = 0; size_t data_cursor = 0; uint32_t repeated_count = 0; }; @@ -595,10 +594,15 @@ struct Ngs2VoiceInternal { return waveform_end; } const auto& block = blocks.front(); - return block.data + (decoder != nullptr - ? block.data_cursor - : (uint64_t(block.info.num_skip_samples) + block.cursor) * - channels * sizeof(int16_t)); + if (decoder != nullptr) { + return block.data + block.data_cursor; + } + // PCM never reads skipped samples, so the next read starts past a pending skip. + const auto skip = block.info.num_samples != 0 && block.data_cursor == 0 + ? block.info.num_skip_samples + : skip_remaining; + return block.data + std::min(block.data_cursor + size_t(skip) * channels * sizeof(int16_t), + block.info.data_size); } // Custom racks keep num_buffers consecutive channel-major buffers; other racks keep one. @@ -687,7 +691,6 @@ struct Ngs2VoiceInternal { 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; @@ -703,10 +706,6 @@ struct Ngs2VoiceInternal { info.num_skip_samples = 0; info.num_samples = 0; } - 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(); @@ -1546,9 +1545,8 @@ static bool Ngs2FinishBlock(Ngs2VoiceInternal& voice) { --block.info.num_repeats; } ++block.repeated_count; - block.cursor = 0; + block.data_cursor = 0; if (voice.decoder != nullptr) { - block.data_cursor = 0; voice.compressed_input.clear(); voice.decoder->Reset(); } @@ -1573,37 +1571,8 @@ static bool Ngs2FinishBlock(Ngs2VoiceInternal& voice) { return voice.source_generation == generation; } -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() && - 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; - } - } - 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(); @@ -1617,6 +1586,41 @@ static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector& frame, } continue; } + if (voice.decoder == nullptr) { + // PCM has no encoded frame size, so stage at most one system grain. + const auto quantum = std::max(voice.rack->ngs->option.num_grain_samples, 1u); + const auto frame_bytes = voice.channels * sizeof(int16_t); + const auto available = (block.info.data_size - block.data_cursor) / frame_bytes; + const auto skip = std::min(voice.skip_remaining, available); + const auto count = + std::min({available - skip, voice.duration_remaining, quantum}); + frame.resize(size_t(quantum) * voice.channels); + const auto* pcm = reinterpret_cast(block.data + block.data_cursor) + + skip * voice.channels; + for (size_t i = 0; i < count * voice.channels; ++i) { + frame[i] = pcm[i] / 32768.0f; + } + voice.skip_remaining -= skip; + voice.duration_remaining -= count; + block.data_cursor += (skip + count) * frame_bytes; + voice.decoded_bytes += (skip + count) * frame_bytes; + voice.decoded_samples += count; + frame_samples = static_cast(count); + if (voice.duration_remaining == 0 || + block.info.data_size - block.data_cursor < frame_bytes) { + // A repeating block restarts its duration, so it must hold all of it. + EXIT_NOT_IMPLEMENTED(voice.duration_remaining != 0 && + (block.data_cursor != block.info.data_size || + block.info.num_repeats != 0)); + if (!Ngs2FinishBlock(voice)) { + return false; + } + } + if (count != 0) { + return true; + } + continue; + } const auto skip = std::min(voice.skip_remaining, static_cast(frame.size() / voice.channels)); diff --git a/tests/Ngs2SamplerTests.cpp b/tests/Ngs2SamplerTests.cpp index 837ff9926..34761c4cd 100644 --- a/tests/Ngs2SamplerTests.cpp +++ b/tests/Ngs2SamplerTests.cpp @@ -124,7 +124,7 @@ void TestWaveformQueueCapacity() { 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), + f.voice.blocks[i].data_cursor == (i == 0 ? sizeof(int16_t) : 0u), "rejected waveform request replaced an outstanding block"); } f.Render(3); @@ -846,13 +846,57 @@ void TestStarvationAndPause() { 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"); + Check(!f.voice.has_samples && f.voice.blocks.front().data_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 TestPcmByteUnitStreaming() { + Fixture f(48000); + const int16_t pcm[] = {0, 4096, 8192, 12288, 16384, 20480, 24576, 28672}; + const Ngs2WaveformBlock first {0, 4, 0, 1, 5, 0, 0}, rest {4, 12, 0, 0, 99, 0, 0}; + Check(f.QueueBlocks(pcm, &first, 1, 1) == OK && f.QueueBlocks(pcm, &rest, 1, 0x12) == OK && + f.voice.WaveformData() == reinterpret_cast(pcm + 1), + "byte-unit stream was rejected or its read address ignored the skipped frame"); + f.Render(8); + Check(f.voice.samples == std::vector {0.125f, 0.25f, 0.375f, 0.5f, 0.625f, 0, 0, 0} && + f.voice.blocks.empty() && f.voice.decoded_samples == 5, + "byte-unit stream did not play exactly its duration across data-only blocks"); +} + +void TestPcmSkipAcrossBlocks() { + Fixture f(48000); + const int16_t pcm[] = {1, 2, 3, 8192, 16384}; + const Ngs2WaveformBlock first {0, 4, 0, 3, 2, 0, 0}, rest {4, 6, 0, 0, 0, 0, 0}; + Check(f.QueueBlocks(pcm, &first, 1, 1) == OK, "skipping stream block was rejected"); + f.Render(1); + Check(!f.voice.has_samples && f.voice.blocks.empty(), "skip-only block produced samples"); + Check(f.QueueBlocks(pcm, &rest, 1, 2) == OK && + f.voice.WaveformData() == reinterpret_cast(pcm + 3), + "read address ignored the skip remaining from the previous block"); + f.Render(2); + Check(f.voice.samples == std::vector {0.25f, 0.5f} && f.voice.decoded_samples == 2 && + f.voice.decoded_bytes == sizeof(pcm), + "skip spanning blocks played skipped frames or miscounted decoded data"); +} + +void TestPcmBlocksEndWithTheirData() { + // The Last Oricru queues blocks holding fewer frames than they declare, even none. + Fixture f(48000, 2); + const int16_t pcm[] = {4096, -4096, 8192, -8192, 12288, -12288}; + const Ngs2WaveformBlock blocks[] = { + {0, 0, 0, 0, 16385, 0, 0}, {0, 8, 0, 0, 4, 0, 0}, {8, 4, 0, 0, 3, 0, 0}}; + Check(f.QueueBlocks(pcm, blocks, 3, 0) == OK, "partially filled blocks were rejected"); + f.Render(5); + Check(f.voice.samples == std::vector {0.125f, 0.25f, 0.375f, 0, 0, -0.125f, -0.25f, + -0.375f, 0, 0} && + f.voice.blocks.empty() && f.voice.decoded_samples == 3, + "partially filled blocks did not play their data back to back"); +} + void TestMonoRateAndRouting() { Fixture f(22050); std::vector pcm(441, 16384); @@ -930,7 +974,7 @@ void TestSamplerReuseResetsRouting() { 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, + Check(!f.voice.has_samples && f.voice.blocks.front().data_cursor == 0, "sampler setup started the new waveform without a play event"); f.voice.ports[0].dest = &mastering; f.voice.SetEvent(1); @@ -1216,7 +1260,7 @@ void TestLoopCallbacksAndExit(Ngs2RackType type) { 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, + active.voice.blocks.front().data_cursor == 0 && active.voice.samples[2] == 0, "loop callback pause consumed another iteration"); Ngs2VoiceParamHeader exit_loop {sizeof(Ngs2VoiceParamHeader), 0, type == Ngs2RackType::Sampler ? 0x10000004u : 0x40010004u}; @@ -1423,7 +1467,7 @@ void TestCallbackStopsConsumption() { "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, + f.voice.blocks.front().data_cursor == 0, "callback pause/stop consumed the next queued block"); if (event == 16) { f.voice.SetEvent(32); @@ -1523,7 +1567,7 @@ void TestCallbackReplacesSource() { 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, + f.voice.blocks.size() == 1 && f.voice.blocks.front().data_cursor == 0, "callback source replacement used an old frame or channel layout"); f.Render(4); const std::vector expected = setup @@ -1684,6 +1728,9 @@ int main() { TestPitchLoopAndSkip(); TestPitchClamps(); TestStarvationAndPause(); + TestPcmByteUnitStreaming(); + TestPcmSkipAcrossBlocks(); + TestPcmBlocksEndWithTheirData(); TestMonoRateAndRouting(); TestMatrixOutputChannels(); TestCustomPcmStillPlays();