mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-28 22:06:59 +01:00
audio: play dataless and streamed NGS2 PCM waveform blocks for Oricru (#1787)
ngs2: stream PCM waveform blocks by byte like ATRAC9 so blocks may hold fewer frames than their duration Co-authored-by: nmzik <Nmzik@mail.ru>
This commit is contained in:
+45
-41
@@ -541,7 +541,6 @@ struct Ngs2VoiceInternal {
|
|||||||
struct Block {
|
struct Block {
|
||||||
const uint8_t* data;
|
const uint8_t* data;
|
||||||
Ngs2WaveformBlock info;
|
Ngs2WaveformBlock info;
|
||||||
uint32_t cursor = 0;
|
|
||||||
size_t data_cursor = 0;
|
size_t data_cursor = 0;
|
||||||
uint32_t repeated_count = 0;
|
uint32_t repeated_count = 0;
|
||||||
};
|
};
|
||||||
@@ -595,10 +594,15 @@ struct Ngs2VoiceInternal {
|
|||||||
return waveform_end;
|
return waveform_end;
|
||||||
}
|
}
|
||||||
const auto& block = blocks.front();
|
const auto& block = blocks.front();
|
||||||
return block.data + (decoder != nullptr
|
if (decoder != nullptr) {
|
||||||
? block.data_cursor
|
return block.data + block.data_cursor;
|
||||||
: (uint64_t(block.info.num_skip_samples) + block.cursor) *
|
}
|
||||||
channels * sizeof(int16_t));
|
// 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.
|
// 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 clear = (flags & 4u) != 0;
|
||||||
const bool only_data = (flags & 2u) != 0;
|
const bool only_data = (flags & 2u) != 0;
|
||||||
const bool join = only_data && (flags & 0x10u) == 0;
|
const bool join = only_data && (flags & 0x10u) == 0;
|
||||||
EXIT_NOT_IMPLEMENTED(only_data && decoder == nullptr);
|
|
||||||
const auto retained = clear ? 0 : blocks.size();
|
const auto retained = clear ? 0 : blocks.size();
|
||||||
Block tail = retained == 0 ? Block {} : blocks.back();
|
Block tail = retained == 0 ? Block {} : blocks.back();
|
||||||
std::vector<Block> pending;
|
std::vector<Block> pending;
|
||||||
@@ -703,10 +706,6 @@ struct Ngs2VoiceInternal {
|
|||||||
info.num_skip_samples = 0;
|
info.num_skip_samples = 0;
|
||||||
info.num_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};
|
Block block {data + info.data_offset, info};
|
||||||
auto* previous = pending.empty() ? (retained == 0 ? nullptr : &tail)
|
auto* previous = pending.empty() ? (retained == 0 ? nullptr : &tail)
|
||||||
: &pending.back();
|
: &pending.back();
|
||||||
@@ -1546,9 +1545,8 @@ static bool Ngs2FinishBlock(Ngs2VoiceInternal& voice) {
|
|||||||
--block.info.num_repeats;
|
--block.info.num_repeats;
|
||||||
}
|
}
|
||||||
++block.repeated_count;
|
++block.repeated_count;
|
||||||
block.cursor = 0;
|
|
||||||
if (voice.decoder != nullptr) {
|
|
||||||
block.data_cursor = 0;
|
block.data_cursor = 0;
|
||||||
|
if (voice.decoder != nullptr) {
|
||||||
voice.compressed_input.clear();
|
voice.compressed_input.clear();
|
||||||
voice.decoder->Reset();
|
voice.decoder->Reset();
|
||||||
}
|
}
|
||||||
@@ -1573,37 +1571,8 @@ static bool Ngs2FinishBlock(Ngs2VoiceInternal& voice) {
|
|||||||
return voice.source_generation == generation;
|
return voice.source_generation == generation;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool Ngs2DecodePcmFrame(Ngs2VoiceInternal& voice, std::vector<float>& 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<const int16_t*>(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<float>& frame,
|
static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector<float>& frame,
|
||||||
uint32_t& frame_samples) {
|
uint32_t& frame_samples) {
|
||||||
if (voice.decoder == nullptr) {
|
|
||||||
return Ngs2DecodePcmFrame(voice, frame, frame_samples);
|
|
||||||
}
|
|
||||||
frame_samples = 0;
|
frame_samples = 0;
|
||||||
while (voice.state == Ngs2VoicePlayState::Playing && !voice.blocks.empty()) {
|
while (voice.state == Ngs2VoicePlayState::Playing && !voice.blocks.empty()) {
|
||||||
auto& block = voice.blocks.front();
|
auto& block = voice.blocks.front();
|
||||||
@@ -1617,6 +1586,41 @@ static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector<float>& frame,
|
|||||||
}
|
}
|
||||||
continue;
|
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<size_t>(voice.skip_remaining, available);
|
||||||
|
const auto count =
|
||||||
|
std::min<size_t>({available - skip, voice.duration_remaining, quantum});
|
||||||
|
frame.resize(size_t(quantum) * voice.channels);
|
||||||
|
const auto* pcm = reinterpret_cast<const int16_t*>(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<uint32_t>(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 =
|
const auto skip =
|
||||||
std::min(voice.skip_remaining,
|
std::min(voice.skip_remaining,
|
||||||
static_cast<uint32_t>(frame.size() / voice.channels));
|
static_cast<uint32_t>(frame.size() / voice.channels));
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ void TestWaveformQueueCapacity() {
|
|||||||
for (uint32_t i = 0; i < capacity; ++i) {
|
for (uint32_t i = 0; i < capacity; ++i) {
|
||||||
Check(f.voice.blocks[i].data == reinterpret_cast<const uint8_t*>(pcm) &&
|
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].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");
|
"rejected waveform request replaced an outstanding block");
|
||||||
}
|
}
|
||||||
f.Render(3);
|
f.Render(3);
|
||||||
@@ -846,13 +846,57 @@ void TestStarvationAndPause() {
|
|||||||
f.Queue(pcm, 2, 0);
|
f.Queue(pcm, 2, 0);
|
||||||
f.voice.SetEvent(16);
|
f.voice.SetEvent(16);
|
||||||
f.Render(2);
|
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.voice.SetEvent(32);
|
||||||
f.Render(2);
|
f.Render(2);
|
||||||
Check(f.voice.samples[0] == 0.5f && f.voice.samples[1] == 0.25f,
|
Check(f.voice.samples[0] == 0.5f && f.voice.samples[1] == 0.25f,
|
||||||
"stream failed to resume after starvation/pause");
|
"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<const uint8_t*>(pcm + 1),
|
||||||
|
"byte-unit stream was rejected or its read address ignored the skipped frame");
|
||||||
|
f.Render(8);
|
||||||
|
Check(f.voice.samples == std::vector<float> {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<const uint8_t*>(pcm + 3),
|
||||||
|
"read address ignored the skip remaining from the previous block");
|
||||||
|
f.Render(2);
|
||||||
|
Check(f.voice.samples == std::vector<float> {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<float> {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() {
|
void TestMonoRateAndRouting() {
|
||||||
Fixture f(22050);
|
Fixture f(22050);
|
||||||
std::vector<int16_t> pcm(441, 16384);
|
std::vector<int16_t> pcm(441, 16384);
|
||||||
@@ -930,7 +974,7 @@ void TestSamplerReuseResetsRouting() {
|
|||||||
const int16_t pcm[] = {8192, 16384, -8192, -16384};
|
const int16_t pcm[] = {8192, 16384, -8192, -16384};
|
||||||
f.Queue(pcm, 2, 0);
|
f.Queue(pcm, 2, 0);
|
||||||
f.Render(1);
|
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");
|
"sampler setup started the new waveform without a play event");
|
||||||
f.voice.ports[0].dest = &mastering;
|
f.voice.ports[0].dest = &mastering;
|
||||||
f.voice.SetEvent(1);
|
f.voice.SetEvent(1);
|
||||||
@@ -1216,7 +1260,7 @@ void TestLoopCallbacksAndExit(Ngs2RackType type) {
|
|||||||
active.Queue(pcm + 2, 1, 0);
|
active.Queue(pcm + 2, 1, 0);
|
||||||
active.Render(6);
|
active.Render(6);
|
||||||
Check(log.events.size() == 1 && active.voice.decoded_samples == 2 &&
|
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");
|
"loop callback pause consumed another iteration");
|
||||||
Ngs2VoiceParamHeader exit_loop {sizeof(Ngs2VoiceParamHeader), 0,
|
Ngs2VoiceParamHeader exit_loop {sizeof(Ngs2VoiceParamHeader), 0,
|
||||||
type == Ngs2RackType::Sampler ? 0x10000004u : 0x40010004u};
|
type == Ngs2RackType::Sampler ? 0x10000004u : 0x40010004u};
|
||||||
@@ -1423,7 +1467,7 @@ void TestCallbackStopsConsumption() {
|
|||||||
"completion callback failed to interrupt playback");
|
"completion callback failed to interrupt playback");
|
||||||
Check(f.voice.samples[0] == 0.0f && 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.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");
|
"callback pause/stop consumed the next queued block");
|
||||||
if (event == 16) {
|
if (event == 16) {
|
||||||
f.voice.SetEvent(32);
|
f.voice.SetEvent(32);
|
||||||
@@ -1523,7 +1567,7 @@ void TestCallbackReplacesSource() {
|
|||||||
f.Render(4);
|
f.Render(4);
|
||||||
Check(!f.voice.has_samples && f.voice.samples.size() == (setup ? 4u : 8u) &&
|
Check(!f.voice.has_samples && f.voice.samples.size() == (setup ? 4u : 8u) &&
|
||||||
std::ranges::all_of(f.voice.samples, [](float value) { return value == 0; }) &&
|
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");
|
"callback source replacement used an old frame or channel layout");
|
||||||
f.Render(4);
|
f.Render(4);
|
||||||
const std::vector<float> expected = setup
|
const std::vector<float> expected = setup
|
||||||
@@ -1684,6 +1728,9 @@ int main() {
|
|||||||
TestPitchLoopAndSkip();
|
TestPitchLoopAndSkip();
|
||||||
TestPitchClamps();
|
TestPitchClamps();
|
||||||
TestStarvationAndPause();
|
TestStarvationAndPause();
|
||||||
|
TestPcmByteUnitStreaming();
|
||||||
|
TestPcmSkipAcrossBlocks();
|
||||||
|
TestPcmBlocksEndWithTheirData();
|
||||||
TestMonoRateAndRouting();
|
TestMonoRateAndRouting();
|
||||||
TestMatrixOutputChannels();
|
TestMatrixOutputChannels();
|
||||||
TestCustomPcmStillPlays();
|
TestCustomPcmStillPlays();
|
||||||
|
|||||||
Reference in New Issue
Block a user