audio: rewind compressed sampler blocks at loop boundaries

This commit is contained in:
nmzik
2026-09-28 16:46:53 +04:00
parent 2016f8e2fe
commit a20b64d3e5
2 changed files with 75 additions and 2 deletions
+69
View File
@@ -712,6 +712,74 @@ void TestLoopCallbacksAndExit() {
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&pending.voice), &exit_loop);
}
void TestAtrac9Looping() {
// Four encoded mono frames with one nonzero spectral coefficient, then superframe padding.
// The block skips one frame plus one sample and ends inside the third frame.
constexpr std::array<uint8_t, 36> frame {0, 0, 0x04, 0x20, 0x04, 0xc0, 0, 0, 0x1f, 0xc0};
std::array<uint8_t, 192> encoded {};
for (size_t i = 0; i < 4; ++i) {
std::copy(std::begin(frame), std::end(frame), encoded.begin() + i * sizeof(frame));
if (i != 0) encoded[i * sizeof(frame)] |= 0x80;
}
const auto make_voice = [&](uint32_t repeats, LoopCallbacks& log) {
auto f = std::make_unique<Fixture>(24000);
f->voice.SetupSampler({NGS2_WAVEFORM_TYPE_ATRAC9, 1, 24000, 0xfe4005f0});
f->voice.state = Ngs2VoicePlayState::Playing;
f->voice.callback = reinterpret_cast<uintptr_t>(&RecordLoopCallback);
f->voice.callback_data = reinterpret_cast<uintptr_t>(&log);
f->voice.callback_flags = 3;
const Ngs2WaveformBlock block {0, encoded.size(), repeats, 129, 137, 0, 123};
struct Blocks {
Ngs2VoiceParamHeader header {32, 0, 0x10000001};
const uint8_t* data;
uint32_t flags, count;
const Ngs2WaveformBlock* blocks;
} param {{32, 0, 0x10000001}, encoded.data(), 0, 1, &block};
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&f->voice), &param.header) == OK,
"compressed loop queue failed");
return f;
};
LoopCallbacks baseline_log, finite_log, split_log, infinite_log;
auto baseline = make_voice(0, baseline_log);
baseline->Render(274);
const auto reference = baseline->voice.samples;
Check(std::ranges::any_of(reference, [](float sample) { return std::abs(sample) > 0.00001f; }),
"synthetic compressed fixture produced no decoded signal");
auto finite = make_voice(2, finite_log);
finite->Render(822);
Check(finite->voice.state == Ngs2VoicePlayState::Empty &&
finite->voice.decoded_samples == 411 && finite->voice.decoded_bytes == 576 &&
finite_log.events == std::vector<std::pair<uint32_t, uint32_t>> {{2, 1}, {2, 2}, {1, 2}},
"compressed finite loop lost samples, input progress, or completion callbacks");
for (size_t i = 0; i < 3; ++i) {
Check(std::equal(reference.begin(), reference.end() - 1,
finite->voice.samples.begin() + i * 274),
"compressed loop did not restore decoder history and skip samples");
const float tail = i == 2 ? reference.back() : std::lerp(reference[272], reference[0], 0.5f);
Check(finite->voice.samples[i * 274 + 273] == tail,
"compressed loop dropped the final frame or broke boundary interpolation");
}
auto split = make_voice(2, split_log);
split->Render(273);
auto joined = split->voice.samples;
split->Render(549);
joined.insert(joined.end(), split->voice.samples.begin(), split->voice.samples.end());
Check(joined == finite->voice.samples && split_log.events == finite_log.events,
"compressed loop changed across render grains");
auto infinite = make_voice(UINT32_MAX, infinite_log);
infinite->Render(300);
Check(infinite->voice.state == Ngs2VoicePlayState::Playing &&
infinite->voice.blocks.front().info.num_repeats == UINT32_MAX,
"compressed infinite loop did not rewind");
Ngs2VoiceParamHeader exit_loop {sizeof(Ngs2VoiceParamHeader), 0, 0x10000004};
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&infinite->voice), &exit_loop);
infinite->Render(248);
Check(infinite->voice.state == Ngs2VoicePlayState::Empty &&
infinite->voice.decoded_samples == 274 &&
infinite_log.events == std::vector<std::pair<uint32_t, uint32_t>> {{2, 1}, {1, 1}},
"compressed loop exit failed to finish the active iteration");
}
void TestWaveformReadAddress() {
Fixture f(48000, 2);
const int16_t pcm[] = {-100, -100, 8192, 4096, 16384, 8192};
@@ -921,6 +989,7 @@ int main() {
TestSamplerReuseResetsRouting();
TestDeferredSamplerSetup();
TestLoopCallbacksAndExit();
TestAtrac9Looping();
TestWaveformReadAddress();
TestFilterTailDuringStarvation();
TestFiniteFilterTail();