mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-20 02:47:02 +02:00
audio: keep NGS2 sampler voices within max_waveform_blocks (#1450)
audio: preserve NGS2 streaming buffer ownership
This commit is contained in:
committed by
GitHub
parent
5ce9a52936
commit
5b0ef07742
+388
-19
@@ -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<uintptr_t>(&voice), &custom.voice_state,
|
||||
sizeof(custom)) == OK, "custom sampler state failed");
|
||||
state.voice_state = custom.voice_state;
|
||||
state.num_decoded_samples = custom.num_decoded_samples;
|
||||
state.decoded_data_size = custom.decoded_data_size;
|
||||
state.waveform_data = custom.waveform_data;
|
||||
state.user_data = custom.user_data;
|
||||
} else {
|
||||
Check(Ngs2VoiceGetState(reinterpret_cast<uintptr_t>(&voice), &state.voice_state,
|
||||
sizeof(state)) == OK, "sampler state failed");
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
int QueueBlocks(const void* data, const Ngs2WaveformBlock* blocks, uint32_t count,
|
||||
uint32_t flags) {
|
||||
struct Blocks {
|
||||
Ngs2VoiceParamHeader header {32, 0, 0x10000001};
|
||||
const void* data;
|
||||
uint32_t flags, count;
|
||||
const Ngs2WaveformBlock* blocks;
|
||||
} param {{32, 0, 0x10000001}, data, flags, 1, &block};
|
||||
} param {{32, 0, 0x10000001}, data, flags, count, blocks};
|
||||
param.header.id = rack.type == Ngs2RackType::Sampler ? 0x10000001 : 0x40010001;
|
||||
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&voice), ¶m.header) == OK,
|
||||
"sampler queue failed");
|
||||
return Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&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<int32_t>(0x804a8054u);
|
||||
constexpr int32_t invalid_count = static_cast<int32_t>(0x804a8053u);
|
||||
for (auto rack: {Ngs2RackType::Sampler, Ngs2RackType::CustomSampler}) {
|
||||
for (uint32_t capacity: {1u, 4u, 16u}) {
|
||||
Fixture f(24000, 1, rack);
|
||||
f.SetCapacity(capacity);
|
||||
f.system.option.num_grain_samples = 1;
|
||||
const int16_t pcm[] = {8192, 16384};
|
||||
const int16_t replacement[] = {24576, 4096};
|
||||
std::vector<Ngs2WaveformBlock> blocks(capacity + 1);
|
||||
for (uint32_t i = 0; i < blocks.size(); ++i) {
|
||||
blocks[i] = {0, sizeof(pcm), 0, 0, 2, 0, i + 1};
|
||||
}
|
||||
Check(f.QueueBlocks(pcm, blocks.data(), capacity, 1) == OK,
|
||||
"configured waveform capacity was not available");
|
||||
f.Render(1);
|
||||
const auto phase = f.voice.sample_phase;
|
||||
Check(f.QueueBlocks(replacement, blocks.data(), 1, 0) == full,
|
||||
"full waveform queue accepted another guest buffer");
|
||||
Check(f.QueueBlocks(replacement, blocks.data(), capacity + 1, 4) == invalid_count,
|
||||
"oversized waveform request did not reject before CLEAR");
|
||||
Check(f.voice.blocks.size() == capacity && f.voice.accepts_blocks &&
|
||||
f.voice.sample_phase == phase && f.voice.decoded_samples == 1 &&
|
||||
f.voice.decoded_bytes == sizeof(int16_t),
|
||||
"rejected waveform request changed playback or stream continuity");
|
||||
for (uint32_t i = 0; i < capacity; ++i) {
|
||||
Check(f.voice.blocks[i].data == reinterpret_cast<const uint8_t*>(pcm) &&
|
||||
f.voice.blocks[i].info.user_data == i + 1 &&
|
||||
f.voice.blocks[i].cursor == (i == 0 ? 1u : 0u),
|
||||
"rejected waveform request replaced an outstanding block");
|
||||
}
|
||||
f.Render(3);
|
||||
Check(f.voice.samples == std::vector<float> {0.375f, 0.5f,
|
||||
capacity == 1 ? 0.5f : 0.375f} &&
|
||||
f.voice.blocks.size() == capacity - 1,
|
||||
"queue rejection changed partially played source audio");
|
||||
Check(f.QueueBlocks(replacement, blocks.data(), 1, 1) == OK &&
|
||||
f.voice.blocks.size() == capacity,
|
||||
"completed waveform block did not release its queue slot");
|
||||
const auto progress = f.State();
|
||||
Check(f.QueueBlocks(replacement, blocks.data(), 1, 5) == OK &&
|
||||
f.voice.blocks.size() == 1 && f.voice.sample_phase == 0 &&
|
||||
f.voice.decoded_samples == progress.num_decoded_samples &&
|
||||
f.voice.decoded_bytes == progress.decoded_data_size,
|
||||
"CLEAR failed to replace a full queue while preserving progress");
|
||||
f.Render(2);
|
||||
Check(f.voice.samples == std::vector<float> {0.75f, 0.4375f},
|
||||
"CLEAR retained an old source or fractional playback position");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PcmRing {
|
||||
static constexpr uint32_t block_frames = 7, block_count = 64;
|
||||
Fixture& fixture;
|
||||
std::array<std::array<int16_t, block_frames * 2>, 4> buffers {};
|
||||
uint32_t next_block = 0, completed = 0;
|
||||
|
||||
void QueueNext(uint32_t slot) {
|
||||
auto& buffer = buffers[slot];
|
||||
for (uint32_t i = 0; i < block_frames; ++i) {
|
||||
buffer[2 * i] = static_cast<int16_t>(100 + (next_block * block_frames + i) * 16);
|
||||
buffer[2 * i + 1] = -buffer[2 * i];
|
||||
}
|
||||
const Ngs2WaveformBlock block {0, sizeof(buffer), 0, 0, block_frames, 0, slot};
|
||||
++next_block;
|
||||
Check(fixture.QueueBlocks(buffer.data(), &block, 1, next_block == block_count ? 0 : 1) == OK,
|
||||
"released ring buffer could not be queued again");
|
||||
}
|
||||
};
|
||||
|
||||
void KYTY_SYSV_ABI RefillPcmRing(const Ngs2VoiceCallbackInfo* info) {
|
||||
auto& ring = *reinterpret_cast<PcmRing*>(info->data);
|
||||
const auto slot = ring.completed++ % ring.buffers.size();
|
||||
Check(info->flag == 1 && info->attributes == 0 && info->user == slot &&
|
||||
info->block_data == ring.buffers[slot].data() &&
|
||||
info->size == sizeof(ring.buffers[slot]),
|
||||
"ring completion callback released the wrong source buffer");
|
||||
for (const auto& block: ring.fixture.voice.blocks) {
|
||||
Check(block.data != reinterpret_cast<const uint8_t*>(ring.buffers[slot].data()),
|
||||
"completion callback retained the buffer about to be overwritten");
|
||||
}
|
||||
if (ring.next_block < PcmRing::block_count) {
|
||||
ring.QueueNext(static_cast<uint32_t>(slot));
|
||||
}
|
||||
}
|
||||
|
||||
void TestWaveformRingReuse() {
|
||||
for (auto rack: {Ngs2RackType::Sampler, Ngs2RackType::CustomSampler}) {
|
||||
Fixture f(48000, 2, rack);
|
||||
PcmRing ring {f};
|
||||
f.voice.callback = reinterpret_cast<uintptr_t>(&RefillPcmRing);
|
||||
f.voice.callback_data = reinterpret_cast<uintptr_t>(&ring);
|
||||
f.voice.callback_flags = 1;
|
||||
for (uint32_t slot = 0; slot < ring.buffers.size(); ++slot) {
|
||||
ring.QueueNext(slot);
|
||||
}
|
||||
constexpr std::array<uint32_t, 6> grains {1, 16, 3, 29, 2, 11};
|
||||
constexpr auto total_frames = PcmRing::block_frames * PcmRing::block_count;
|
||||
uint32_t rendered = 0, grain_index = 0;
|
||||
while (rendered < total_frames) {
|
||||
const auto grain = std::min(grains[grain_index++ % grains.size()], total_frames - rendered);
|
||||
f.Render(grain);
|
||||
for (uint32_t i = 0; i < grain; ++i) {
|
||||
const float expected = (100 + (rendered + i) * 16) / 32768.0f;
|
||||
Check(f.voice.samples[i] == expected && f.voice.samples[grain + i] == -expected,
|
||||
"ring buffer reuse replayed overwritten data or mixed stereo channels");
|
||||
}
|
||||
rendered += grain;
|
||||
const auto decoded = f.State().num_decoded_samples;
|
||||
Check(f.voice.blocks.size() <= ring.buffers.size() &&
|
||||
decoded >= rendered && decoded - rendered < 256 &&
|
||||
f.voice.decoded_bytes == decoded * 4 &&
|
||||
ring.completed == decoded / PcmRing::block_frames,
|
||||
"ring playback lost queue capacity or decoded progress");
|
||||
if (decoded < total_frames) {
|
||||
const auto slot = (decoded / PcmRing::block_frames) % ring.buffers.size();
|
||||
Check(f.voice.WaveformData() == reinterpret_cast<const uint8_t*>(
|
||||
ring.buffers[slot].data() + (decoded % PcmRing::block_frames) * 2),
|
||||
"ring read address did not track the active source buffer");
|
||||
}
|
||||
}
|
||||
Check(f.voice.blocks.empty() && f.voice.state == Ngs2VoicePlayState::Empty &&
|
||||
ring.completed == PcmRing::block_count,
|
||||
"finite ring stream did not release all buffers and finish");
|
||||
}
|
||||
}
|
||||
|
||||
void TestProgressDrivenWaveformRing() {
|
||||
const auto source_sample = [](uint64_t frame) {
|
||||
return static_cast<int16_t>(static_cast<int32_t>((frame * 43) % 32768) - 16384);
|
||||
};
|
||||
for (auto rack: {Ngs2RackType::Sampler, Ngs2RackType::CustomSampler}) {
|
||||
Fixture f(44100, 2, rack);
|
||||
std::array<std::array<int16_t, 1024 * 2>, 4> buffers {};
|
||||
uint64_t submitted = 0, decoded_bytes = 0, rendered = 0;
|
||||
uint32_t slot = 0;
|
||||
const auto refill = [&](uint32_t frames) {
|
||||
auto& buffer = buffers[slot];
|
||||
for (const auto& block: f.voice.blocks) {
|
||||
Check(block.data != reinterpret_cast<const uint8_t*>(buffer.data()),
|
||||
"decoded progress allowed the guest to overwrite an outstanding ring buffer");
|
||||
}
|
||||
for (uint32_t i = 0; i < frames; ++i) {
|
||||
buffer[i * 2] = source_sample(submitted + i);
|
||||
buffer[i * 2 + 1] = -buffer[i * 2];
|
||||
}
|
||||
const Ngs2WaveformBlock block {0, frames * 4u, 0, 0, frames, 0, slot};
|
||||
Check(f.QueueBlocks(buffer.data(), &block, 1, 1) == OK,
|
||||
"progress-driven ring exhausted its waveform slots");
|
||||
submitted += frames;
|
||||
slot = (slot + 1) % buffers.size();
|
||||
};
|
||||
refill(1024);
|
||||
for (uint32_t grain = 0; grain < 128; ++grain) {
|
||||
f.Render(256);
|
||||
for (uint32_t i = 0; i < 256; ++i) {
|
||||
const uint64_t phase = (rendered + i) * 44100;
|
||||
const auto frame = phase / 48000;
|
||||
const float fraction = static_cast<float>(phase % 48000) / 48000.0f;
|
||||
const float expected = std::lerp(static_cast<float>(source_sample(frame)),
|
||||
static_cast<float>(source_sample(frame + 1)), fraction) /
|
||||
32768.0f;
|
||||
Check(std::abs(f.voice.samples[i] - expected) < 0.00001f &&
|
||||
std::abs(f.voice.samples[256 + i] + expected) < 0.00001f,
|
||||
"progress-driven ring replayed overwritten music samples");
|
||||
}
|
||||
rendered += 256;
|
||||
const auto state = f.State();
|
||||
const auto delta = state.decoded_data_size - decoded_bytes;
|
||||
Check(state.decoded_data_size >= decoded_bytes && delta <= sizeof(buffers[0]) &&
|
||||
delta % 4 == 0 && state.num_decoded_samples * 4 == state.decoded_data_size &&
|
||||
state.num_decoded_samples <= submitted,
|
||||
"published sampler progress does not describe decoded source data");
|
||||
decoded_bytes = state.decoded_data_size;
|
||||
if (delta != 0) {
|
||||
refill(static_cast<uint32_t>(delta / 4));
|
||||
}
|
||||
}
|
||||
Check(decoded_bytes > 28 * sizeof(buffers[0]), "ring test did not exercise repeated buffer reuse");
|
||||
}
|
||||
}
|
||||
|
||||
void TestStreamingResample() {
|
||||
Fixture f(44100, 2);
|
||||
std::vector<int16_t> pcm(441 * 2);
|
||||
@@ -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<float> {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<float> {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<uintptr_t>(&active.voice), &exit_loop);
|
||||
active.voice.SetEvent(32);
|
||||
active.Render(3);
|
||||
Check(active.voice.samples == std::vector<float> {0.25f, 0.5f, 0.75f} &&
|
||||
active.Render(5);
|
||||
Check(active.voice.samples == std::vector<float> {0.25f, 0.5f, 0.25f, 0.5f, 0.75f} &&
|
||||
active.voice.blocks.empty() && log.events.size() == 1,
|
||||
"exiting infinite loop lost its final iteration or ignored callback flags");
|
||||
|
||||
@@ -858,8 +1078,69 @@ void TestAtrac9Looping() {
|
||||
"compressed loop exit failed to finish the active iteration");
|
||||
}
|
||||
|
||||
void TestAtrac9WaveformJoining() {
|
||||
std::array<uint8_t, 192> encoded {};
|
||||
for (size_t i = 0; i < 4; ++i) {
|
||||
std::copy(ATRAC9_MONO_BLOCK.begin(), ATRAC9_MONO_BLOCK.end(),
|
||||
encoded.begin() + i * ATRAC9_MONO_BLOCK.size());
|
||||
if (i != 0) encoded[i * ATRAC9_MONO_BLOCK.size()] |= 0x80;
|
||||
}
|
||||
for (auto rack: {Ngs2RackType::Sampler, Ngs2RackType::CustomSampler}) {
|
||||
Fixture reference(24000, 1, rack);
|
||||
reference.voice.SetupSampler({NGS2_WAVEFORM_TYPE_ATRAC9, 1, 24000, 0xfe4005f0});
|
||||
reference.voice.state = Ngs2VoicePlayState::Playing;
|
||||
const Ngs2WaveformBlock whole {0, encoded.size(), 0, 0, 512, 0, 123};
|
||||
Check(reference.QueueBlocks(encoded.data(), &whole, 1, 0) == OK,
|
||||
"compressed reference block could not be queued");
|
||||
reference.Render(1024);
|
||||
for (bool nojoin: {false, true}) {
|
||||
Fixture f(24000, 1, rack);
|
||||
f.SetCapacity(1);
|
||||
f.voice.SetupSampler({NGS2_WAVEFORM_TYPE_ATRAC9, 1, 24000, 0xfe4005f0});
|
||||
f.voice.state = Ngs2VoicePlayState::Playing;
|
||||
LoopCallbacks callbacks;
|
||||
f.voice.callback = reinterpret_cast<uintptr_t>(&RecordLoopCallback);
|
||||
f.voice.callback_data = reinterpret_cast<uintptr_t>(&callbacks);
|
||||
f.voice.callback_flags = 1;
|
||||
const Ngs2WaveformBlock first {0, 36, 0, 0, 512, 0, 123};
|
||||
const Ngs2WaveformBlock rest {36, encoded.size() - 36, 99, 99, 99, 0, 123};
|
||||
Check(f.QueueBlocks(encoded.data(), &first, 1, 1) == OK,
|
||||
"compressed stream start could not be queued");
|
||||
const uint32_t flags = nojoin ? 0x12 : 2;
|
||||
if (nojoin) {
|
||||
Check(f.QueueBlocks(encoded.data(), &rest, 1, flags) ==
|
||||
static_cast<int32_t>(0x804a8054u) &&
|
||||
f.voice.blocks.size() == 1 && f.voice.blocks.front().info.data_size == 36 &&
|
||||
f.voice.accepts_blocks,
|
||||
"NOJOINT bypassed capacity or changed the full stream");
|
||||
f.SetCapacity(2);
|
||||
}
|
||||
Check(f.QueueBlocks(encoded.data(), &rest, 1, flags) == OK &&
|
||||
f.voice.blocks.size() == (nojoin ? 2u : 1u),
|
||||
"consecutive ONLY_DATA blocks did not follow the joining flag");
|
||||
f.Render(1024);
|
||||
Check(f.voice.samples == reference.voice.samples && f.voice.decoded_samples == 512 &&
|
||||
f.voice.decoded_bytes == encoded.size() && f.voice.blocks.empty() &&
|
||||
callbacks.events.size() == (nojoin ? 2u : 1u),
|
||||
"compressed joining changed audio, progress, or block completion callbacks");
|
||||
}
|
||||
Fixture full(24000, 1, rack);
|
||||
full.SetCapacity(2);
|
||||
full.voice.SetupSampler({NGS2_WAVEFORM_TYPE_ATRAC9, 1, 24000, 0xfe4005f0});
|
||||
const Ngs2WaveformBlock initial[] = {{0, 36, 0, 0, 512}, {36, 36, 0, 0, 512}};
|
||||
const Ngs2WaveformBlock addition[] = {{72, 36}, {144, 48}};
|
||||
Check(full.QueueBlocks(encoded.data(), initial, 2, 1) == OK &&
|
||||
full.QueueBlocks(encoded.data(), addition, 2, 2) ==
|
||||
static_cast<int32_t>(0x804a8054u) &&
|
||||
full.voice.blocks.size() == 2 && full.voice.blocks.back().info.data_size == 36 &&
|
||||
full.voice.accepts_blocks,
|
||||
"overflow after a tentative join partially extended or closed the stream");
|
||||
}
|
||||
}
|
||||
|
||||
void TestWaveformReadAddress() {
|
||||
Fixture f(48000, 2);
|
||||
f.system.option.num_grain_samples = 1;
|
||||
const int16_t pcm[] = {-100, -100, 8192, 4096, 16384, 8192};
|
||||
f.Queue(pcm, 2, 1, 1, 1);
|
||||
const auto next_data = [&]() {
|
||||
@@ -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<CallbackEvent*>(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<float> {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<uintptr_t>(&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<uintptr_t>(&ApplyCallbackEvent);
|
||||
f.voice.callback_data = reinterpret_cast<uintptr_t>(&context);
|
||||
f.voice.callback_flags = 1;
|
||||
f.Render(1);
|
||||
Check(f.voice.samples[0] == 0.25f, "initial source sample was not rendered");
|
||||
f.Render(2);
|
||||
Check(!f.voice.has_samples && f.voice.state == Ngs2VoicePlayState::Paused &&
|
||||
context.calls == 2 && f.State().num_decoded_samples == 2 &&
|
||||
f.voice.sample_phase == (uint64_t(24000) << 32u),
|
||||
"lookahead callback pause consumed output or lost decoded progress");
|
||||
pcm[0] = pcm[1] = -32768;
|
||||
f.voice.SetEvent(32);
|
||||
f.Render(3);
|
||||
Check(f.voice.samples == std::vector<float> {0.375f, 0.5f, 0.5f} &&
|
||||
f.voice.state == Ngs2VoicePlayState::Empty,
|
||||
"lookahead pause failed to preserve owned samples or fractional playback position");
|
||||
}
|
||||
}
|
||||
|
||||
struct CallbackReplacement {
|
||||
Fixture& fixture;
|
||||
bool setup;
|
||||
uint32_t trigger, calls = 0;
|
||||
const std::array<int16_t, 4> pcm {16384, 8192, 4096, 2048};
|
||||
};
|
||||
|
||||
void KYTY_SYSV_ABI ReplaceCallbackSource(const Ngs2VoiceCallbackInfo* info) {
|
||||
auto& replacement = *reinterpret_cast<CallbackReplacement*>(info->data);
|
||||
if (++replacement.calls != replacement.trigger) {
|
||||
return;
|
||||
}
|
||||
auto& f = replacement.fixture;
|
||||
f.voice.callback_flags = 0;
|
||||
if (replacement.setup) {
|
||||
f.voice.SetupSampler({0x12, 1, 48000});
|
||||
}
|
||||
f.Queue(replacement.pcm.data(), 2, replacement.setup ? 0 : 4);
|
||||
f.voice.SetEvent(1);
|
||||
}
|
||||
|
||||
void TestCallbackReplacesSource() {
|
||||
for (auto rack: {Ngs2RackType::Sampler, Ngs2RackType::CustomSampler}) {
|
||||
for (bool setup: {false, true}) {
|
||||
for (uint32_t trigger: {1u, 2u}) {
|
||||
Fixture f(24000, 2, rack);
|
||||
f.system.option.num_grain_samples = 1;
|
||||
CallbackReplacement replacement {f, setup, trigger};
|
||||
const int16_t pcm[] = {8192, 4096};
|
||||
for (uint32_t i = 0; i < trigger; ++i) {
|
||||
f.Queue(pcm, 1, 1);
|
||||
}
|
||||
f.voice.callback = reinterpret_cast<uintptr_t>(&ReplaceCallbackSource);
|
||||
f.voice.callback_data = reinterpret_cast<uintptr_t>(&replacement);
|
||||
f.voice.callback_flags = 1;
|
||||
f.Render(4);
|
||||
Check(!f.voice.has_samples && f.voice.samples.size() == (setup ? 4u : 8u) &&
|
||||
std::ranges::all_of(f.voice.samples, [](float value) { return value == 0; }) &&
|
||||
f.voice.blocks.size() == 1 && f.voice.blocks.front().cursor == 0,
|
||||
"callback source replacement used an old frame or channel layout");
|
||||
f.Render(4);
|
||||
const std::vector<float> expected = setup
|
||||
? std::vector<float> {0.5f, 0.25f, 0, 0}
|
||||
: std::vector<float> {0.5f, 0.3125f, 0.125f, 0.125f,
|
||||
0.25f, 0.15625f, 0.0625f, 0.0625f};
|
||||
Check(f.voice.samples == expected && f.voice.state == Ngs2VoicePlayState::Empty,
|
||||
"callback replacement failed to play the new source on the next render");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TestUpstreamCustomSamplerControls() {
|
||||
Fixture f(24000, 1, Ngs2RackType::CustomSampler);
|
||||
const int16_t pcm[] = {8192, 16384, 24576, 4096};
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user