mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-19 18:36:53 +02:00
audio: improve NGS2 stream and ATRAC9 playback
This commit is contained in:
+342
-104
@@ -11,6 +11,7 @@
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <limits>
|
||||
@@ -197,8 +198,8 @@ struct Ngs2WaveformFormat {
|
||||
uint32_t num_channels = 0;
|
||||
uint32_t sample_rate = 0;
|
||||
uint32_t config_data = 0;
|
||||
uint32_t frame_margin = 0;
|
||||
uint32_t frame_offset = 0;
|
||||
uint32_t frame_margin = 0;
|
||||
};
|
||||
|
||||
struct Ngs2WaveformBlock {
|
||||
@@ -388,18 +389,20 @@ struct Ngs2RackInternal {
|
||||
|
||||
enum class Ngs2VoicePlayState : uint32_t { Empty = 0, Playing = 3, Paused = 5, Stopped = 0xb };
|
||||
|
||||
// The 64-bit bypass mask precedes the filter type and FCQ parameters.
|
||||
struct Ngs2SamplerFilterParam {
|
||||
uint32_t index;
|
||||
uint32_t location;
|
||||
uint64_t channel_mask;
|
||||
uint32_t type;
|
||||
uint64_t channel_mask;
|
||||
float frequency;
|
||||
float q;
|
||||
float level;
|
||||
uint32_t reserved[3];
|
||||
};
|
||||
static_assert(sizeof(Ngs2SamplerFilterParam) == 48);
|
||||
static_assert(offsetof(Ngs2SamplerFilterParam, type) == 8 &&
|
||||
offsetof(Ngs2SamplerFilterParam, channel_mask) == 16 &&
|
||||
offsetof(Ngs2SamplerFilterParam, frequency) == 24);
|
||||
|
||||
struct Ngs2SamplerFilter {
|
||||
struct History {
|
||||
@@ -507,7 +510,6 @@ struct Ngs2VoiceInternal {
|
||||
Ngs2WaveformBlock info;
|
||||
uint32_t cursor = 0;
|
||||
size_t data_cursor = 0;
|
||||
uint32_t skip_remaining = info.num_skip_samples;
|
||||
uint32_t repeated_count = 0;
|
||||
};
|
||||
struct Module {
|
||||
@@ -535,14 +537,21 @@ struct Ngs2VoiceInternal {
|
||||
bool has_samples = false;
|
||||
|
||||
std::unique_ptr<Ajm::AjmAt9Decoder> decoder;
|
||||
std::vector<float> decoded_frame;
|
||||
bool accepts_blocks = true;
|
||||
uint32_t sample_rate = 0;
|
||||
uint64_t sample_phase = 0;
|
||||
uint64_t sample_step = 0;
|
||||
uint64_t decoded_samples = 0;
|
||||
uint64_t decoded_bytes = 0;
|
||||
const uint8_t* waveform_end = nullptr;
|
||||
std::vector<float> decoded_frame;
|
||||
std::vector<float> next_decoded_frame;
|
||||
std::vector<uint8_t> compressed_input;
|
||||
uint32_t frame_cursor = 0;
|
||||
uint32_t frame_samples = 0;
|
||||
uint32_t next_frame_samples = 0;
|
||||
bool accepts_blocks = true;
|
||||
uint32_t sample_rate = 0;
|
||||
uint64_t sample_phase = 0;
|
||||
uint64_t sample_step = 0;
|
||||
uint64_t decoded_samples = 0;
|
||||
uint64_t decoded_bytes = 0;
|
||||
uint32_t duration_remaining = 0;
|
||||
uint32_t skip_remaining = 0;
|
||||
const uint8_t* waveform_end = nullptr;
|
||||
std::vector<Ngs2SamplerFilter> filters;
|
||||
|
||||
const uint8_t* WaveformData() const {
|
||||
@@ -556,28 +565,44 @@ struct Ngs2VoiceInternal {
|
||||
channels * sizeof(int16_t));
|
||||
}
|
||||
|
||||
void SetupSampler(const Ngs2WaveformFormat& format) {
|
||||
EXIT_NOT_IMPLEMENTED(format.frame_margin != 0 || format.frame_offset != 0 ||
|
||||
format.sample_rate == 0 || format.num_channels == 0 ||
|
||||
(format.waveform_type == NGS2_WAVEFORM_TYPE_ATRAC9 &&
|
||||
format.sample_rate != rack->ngs->option.sample_rate));
|
||||
void ResetSetupState() {
|
||||
SetEvent(4);
|
||||
std::ranges::fill(ports, Port {});
|
||||
for (auto& matrix: matrices) {
|
||||
matrix.clear();
|
||||
}
|
||||
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;
|
||||
waveform_end = nullptr;
|
||||
blocks.clear();
|
||||
filters.clear();
|
||||
}
|
||||
|
||||
void SetupSampler(const Ngs2WaveformFormat& format) {
|
||||
const bool deferred = format.waveform_type == 0 && format.num_channels == 0 &&
|
||||
format.sample_rate == 0 && format.config_data == 0 &&
|
||||
format.frame_margin == 0 && format.frame_offset == 0;
|
||||
EXIT_NOT_IMPLEMENTED(!deferred &&
|
||||
(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();
|
||||
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;
|
||||
}
|
||||
if (format.waveform_type == NGS2_WAVEFORM_TYPE_ATRAC9) {
|
||||
decoder = std::make_unique<Ajm::AjmAt9Decoder>(channels, format.sample_rate,
|
||||
Ajm::AjmSampleEncoding::Float, 0);
|
||||
@@ -594,6 +619,7 @@ struct Ngs2VoiceInternal {
|
||||
decoder->WriteCodecInfo(&info, sizeof(info), result);
|
||||
EXIT_NOT_IMPLEMENTED(info.frame_samples > std::numeric_limits<uint16_t>::max());
|
||||
decoded_frame.resize(info.frame_samples * channels);
|
||||
compressed_input.reserve(info.super_frame_size);
|
||||
} else {
|
||||
EXIT_NOT_IMPLEMENTED(format.waveform_type != 0x12);
|
||||
}
|
||||
@@ -653,6 +679,13 @@ struct Ngs2VoiceEventParam {
|
||||
uint32_t event_id;
|
||||
};
|
||||
|
||||
struct Ngs2SubmixerVoiceSetupParam {
|
||||
Ngs2VoiceParamHeader header;
|
||||
uint32_t num_io_channels;
|
||||
uint32_t flags;
|
||||
};
|
||||
static_assert(sizeof(Ngs2SubmixerVoiceSetupParam) == 16);
|
||||
|
||||
struct Ngs2VoicePatchParam {
|
||||
Ngs2VoiceParamHeader header;
|
||||
uint32_t port;
|
||||
@@ -749,6 +782,8 @@ static_assert(sizeof(Ngs2CustomMasteringVoiceState) == 16);
|
||||
static_assert(sizeof(Ngs2SamplerVoiceState) == 56);
|
||||
static_assert(sizeof(Ngs2CustomSamplerVoiceState) == 48);
|
||||
static_assert(sizeof(Ngs2WaveformFormat) == 24);
|
||||
static_assert(offsetof(Ngs2WaveformFormat, frame_offset) == 16 &&
|
||||
offsetof(Ngs2WaveformFormat, frame_margin) == 20);
|
||||
static_assert(sizeof(Ngs2WaveformBlock) == 40);
|
||||
static_assert(sizeof(Ngs2WaveformInfo) == 232);
|
||||
|
||||
@@ -1383,10 +1418,12 @@ struct Ngs2VoiceCallbackInfo {
|
||||
uint32_t flag, reserved;
|
||||
uintptr_t user;
|
||||
const void* block_data;
|
||||
size_t size;
|
||||
uint32_t repeats, attributes;
|
||||
uint32_t size, repeats, attributes, reserved2;
|
||||
};
|
||||
static_assert(sizeof(Ngs2VoiceCallbackInfo) == 56);
|
||||
static_assert(offsetof(Ngs2VoiceCallbackInfo, size) == 40 &&
|
||||
offsetof(Ngs2VoiceCallbackInfo, repeats) == 44 &&
|
||||
offsetof(Ngs2VoiceCallbackInfo, attributes) == 48);
|
||||
|
||||
static void Ngs2FinishBlock(Ngs2VoiceInternal& voice) {
|
||||
auto& block = voice.blocks.front();
|
||||
@@ -1405,8 +1442,9 @@ static void Ngs2FinishBlock(Ngs2VoiceInternal& voice) {
|
||||
0,
|
||||
block.info.user_data,
|
||||
block.data,
|
||||
block.info.data_size,
|
||||
static_cast<uint32_t>(block.info.data_size),
|
||||
block.repeated_count,
|
||||
0,
|
||||
0};
|
||||
if (!repeat) {
|
||||
voice.blocks.pop_front();
|
||||
@@ -1439,9 +1477,123 @@ static void Ngs2AdvancePcm(Ngs2VoiceInternal& voice) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector<float>& frame,
|
||||
uint32_t& frame_samples) {
|
||||
while (voice.state == Ngs2VoicePlayState::Playing && !voice.blocks.empty()) {
|
||||
auto& block = voice.blocks.front();
|
||||
if (block.info.num_samples != 0 && block.data_cursor == 0) {
|
||||
voice.duration_remaining = block.info.num_samples;
|
||||
voice.skip_remaining = block.info.num_skip_samples;
|
||||
}
|
||||
if (voice.duration_remaining == 0) {
|
||||
Ngs2FinishBlock(voice);
|
||||
continue;
|
||||
}
|
||||
const auto skip =
|
||||
std::min(voice.skip_remaining,
|
||||
static_cast<uint32_t>(frame.size() / voice.channels));
|
||||
Ajm::AjmGaplessState gapless;
|
||||
gapless.Set({voice.duration_remaining, static_cast<uint16_t>(skip), 0}, true);
|
||||
if (!voice.compressed_input.empty()) {
|
||||
Ajm::AjmSidebandDecAt9CodecInfo info {};
|
||||
voice.decoder->WriteCodecInfo(&info, sizeof(info), {});
|
||||
EXIT_NOT_IMPLEMENTED(info.next_frame_size < voice.compressed_input.size());
|
||||
const auto bytes =
|
||||
std::min<size_t>(info.next_frame_size - voice.compressed_input.size(),
|
||||
block.info.data_size - block.data_cursor);
|
||||
voice.compressed_input.insert(voice.compressed_input.end(),
|
||||
block.data + block.data_cursor,
|
||||
block.data + block.data_cursor + bytes);
|
||||
block.data_cursor += bytes;
|
||||
voice.decoded_bytes += bytes;
|
||||
if (voice.compressed_input.size() < info.next_frame_size) {
|
||||
Ngs2FinishBlock(voice);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const bool pending = !voice.compressed_input.empty();
|
||||
const auto result = voice.decoder->Decode(
|
||||
pending ? voice.compressed_input.data() : block.data + block.data_cursor,
|
||||
pending ? voice.compressed_input.size() : block.info.data_size - block.data_cursor,
|
||||
frame.data(), frame.size() * sizeof(float), false,
|
||||
&gapless);
|
||||
if (!pending && result.result == Ajm::AJM_RESULT_PARTIAL_INPUT &&
|
||||
result.input_consumed == 0) {
|
||||
voice.compressed_input.insert(voice.compressed_input.end(),
|
||||
block.data + block.data_cursor,
|
||||
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);
|
||||
continue;
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(result.result != OK || result.input_consumed == 0);
|
||||
if (pending) {
|
||||
voice.compressed_input.erase(voice.compressed_input.begin(),
|
||||
voice.compressed_input.begin() + result.input_consumed);
|
||||
} else {
|
||||
block.data_cursor += result.input_consumed;
|
||||
voice.decoded_bytes += result.input_consumed;
|
||||
}
|
||||
voice.skip_remaining -= skip - gapless.current.skip_samples;
|
||||
const auto decoded_samples =
|
||||
static_cast<uint32_t>(result.output_written / (sizeof(float) * voice.channels));
|
||||
if (decoded_samples != 0) {
|
||||
EXIT_NOT_IMPLEMENTED(decoded_samples > voice.duration_remaining);
|
||||
frame_samples = decoded_samples;
|
||||
voice.duration_remaining -= 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 (decoded_samples != 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool Ngs2CurrentAtracFrame(Ngs2VoiceInternal& voice) {
|
||||
if (voice.frame_cursor == voice.frame_samples) {
|
||||
if (voice.next_frame_samples != 0) {
|
||||
std::swap(voice.decoded_frame, voice.next_decoded_frame);
|
||||
voice.frame_samples = voice.next_frame_samples;
|
||||
voice.next_frame_samples = 0;
|
||||
} 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) {
|
||||
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)) {
|
||||
break;
|
||||
}
|
||||
++voice.frame_cursor;
|
||||
++voice.decoded_samples;
|
||||
voice.sample_phase -= output_rate;
|
||||
}
|
||||
if (voice.blocks.empty() && voice.frame_cursor == voice.frame_samples &&
|
||||
voice.next_frame_samples == 0 && !voice.accepts_blocks) {
|
||||
voice.sample_phase = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) {
|
||||
uint32_t output = 0;
|
||||
while (voice.state == Ngs2VoicePlayState::Playing && output < grain && !voice.blocks.empty()) {
|
||||
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()) {
|
||||
@@ -1461,8 +1613,7 @@ static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) {
|
||||
next_block.info.num_skip_samples * voice.channels;
|
||||
}
|
||||
const float fraction = static_cast<float>(voice.sample_phase) /
|
||||
static_cast<float>(uint64_t(voice.rack->ngs->option.sample_rate)
|
||||
<< 32u);
|
||||
static_cast<float>(output_rate);
|
||||
for (uint32_t c = 0; c < voice.channels; ++c) {
|
||||
voice.samples[c * grain + output] =
|
||||
std::lerp(static_cast<float>(pcm[c]), static_cast<float>(next[c]), fraction) /
|
||||
@@ -1472,44 +1623,39 @@ static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) {
|
||||
++output;
|
||||
continue;
|
||||
}
|
||||
auto& block = voice.blocks.front();
|
||||
auto count = std::min(grain - output, block.info.num_samples - block.cursor);
|
||||
const auto skip =
|
||||
std::min(block.skip_remaining,
|
||||
static_cast<uint32_t>(voice.decoded_frame.size() / voice.channels));
|
||||
Ajm::AjmGaplessState gapless;
|
||||
gapless.Set({block.info.num_samples - block.cursor, static_cast<uint16_t>(skip), 0},
|
||||
true);
|
||||
const auto result = voice.decoder->Decode(
|
||||
block.data + block.data_cursor, block.info.data_size - block.data_cursor,
|
||||
voice.decoded_frame.data(), voice.decoded_frame.size() * sizeof(float), false,
|
||||
&gapless);
|
||||
EXIT_NOT_IMPLEMENTED(result.result != OK || result.input_consumed == 0);
|
||||
block.data_cursor += result.input_consumed;
|
||||
block.skip_remaining -= skip - gapless.current.skip_samples;
|
||||
const auto decoded_samples =
|
||||
static_cast<uint32_t>(result.output_written / (sizeof(float) * voice.channels));
|
||||
if (decoded_samples == 0) {
|
||||
continue;
|
||||
Ngs2AdvanceAtrac9(voice);
|
||||
if (!Ngs2CurrentAtracFrame(voice)) {
|
||||
break;
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(decoded_samples > count);
|
||||
count = decoded_samples;
|
||||
for (uint32_t c = 0; c < voice.channels; ++c) {
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
const auto index = i * voice.channels + c;
|
||||
voice.samples[c * grain + output + i] = voice.decoded_frame[index];
|
||||
if (voice.frame_cursor + 1 == voice.frame_samples && voice.sample_phase != 0) {
|
||||
if (voice.next_frame_samples == 0 && !voice.blocks.empty()) {
|
||||
if (voice.next_decoded_frame.empty()) {
|
||||
voice.next_decoded_frame.resize(voice.decoded_frame.size());
|
||||
}
|
||||
Ngs2DecodeFrame(voice, voice.next_decoded_frame, voice.next_frame_samples);
|
||||
}
|
||||
if (voice.state != Ngs2VoicePlayState::Playing ||
|
||||
voice.frame_cursor >= voice.frame_samples) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
output += count;
|
||||
block.cursor += count;
|
||||
voice.decoded_samples += count;
|
||||
if (block.cursor == block.info.num_samples) {
|
||||
voice.decoded_bytes += block.info.data_size;
|
||||
Ngs2FinishBlock(voice);
|
||||
const float* current = voice.decoded_frame.data() + voice.frame_cursor * voice.channels;
|
||||
const float* next = voice.frame_cursor + 1 < voice.frame_samples
|
||||
? current + voice.channels
|
||||
: voice.next_frame_samples != 0 ? voice.next_decoded_frame.data()
|
||||
: current;
|
||||
const float fraction = static_cast<float>(voice.sample_phase) /
|
||||
static_cast<float>(output_rate);
|
||||
for (uint32_t c = 0; c < voice.channels; ++c) {
|
||||
voice.samples[c * grain + output] = std::lerp(current[c], next[c], fraction);
|
||||
}
|
||||
voice.sample_phase += voice.sample_step;
|
||||
++output;
|
||||
}
|
||||
if (voice.decoder == nullptr) {
|
||||
Ngs2AdvancePcm(voice);
|
||||
} else {
|
||||
Ngs2AdvanceAtrac9(voice);
|
||||
}
|
||||
voice.has_samples = output != 0;
|
||||
}
|
||||
@@ -1519,6 +1665,11 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vector<Ngs2Voic
|
||||
if (voice.rendered) {
|
||||
return;
|
||||
}
|
||||
if (voice.channels == 0) {
|
||||
voice.has_samples = false;
|
||||
voice.rendered = true;
|
||||
return;
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(voice.rendering);
|
||||
voice.rendering = true;
|
||||
voice.samples.assign(grain * voice.channels, 0.0f);
|
||||
@@ -1535,6 +1686,8 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vector<Ngs2Voic
|
||||
}
|
||||
}
|
||||
if (voice.state == Ngs2VoicePlayState::Playing && voice.blocks.empty() &&
|
||||
voice.frame_cursor == voice.frame_samples &&
|
||||
voice.next_frame_samples == 0 &&
|
||||
!voice.accepts_blocks &&
|
||||
std::ranges::none_of(voice.filters,
|
||||
[](const auto& filter) { return filter.HasHistory(); })) {
|
||||
@@ -1552,12 +1705,16 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vector<Ngs2Voic
|
||||
}
|
||||
EXIT_NOT_IMPLEMENTED(port.input != 0);
|
||||
const auto* matrix = port.matrix < 0 ? nullptr : &source->matrices.at(port.matrix);
|
||||
for (uint32_t dst = 0; dst < voice.channels; ++dst) {
|
||||
const size_t output_channels =
|
||||
matrix == nullptr
|
||||
? voice.channels
|
||||
: std::min<size_t>(voice.channels, matrix->size() / source->channels);
|
||||
for (size_t dst = 0; dst < output_channels; ++dst) {
|
||||
for (uint32_t src = 0; src < source->channels; ++src) {
|
||||
const float level =
|
||||
port.volume * (matrix == nullptr
|
||||
? (src == dst ? 1.0f : 0.0f)
|
||||
: matrix->at(dst * source->channels + src));
|
||||
: (*matrix)[dst * source->channels + src]);
|
||||
for (uint32_t i = 0; i < grain; ++i) {
|
||||
voice.samples[dst * grain + i] +=
|
||||
source->samples[src * grain + i] * level;
|
||||
@@ -1612,6 +1769,7 @@ int KYTY_SYSV_ABI Ngs2SystemRender(uintptr_t system_handle, const Ngs2RenderBuff
|
||||
EXIT_NOT_IMPLEMENTED(buffer_info == nullptr || system_handle == 0 || num_buffer_info == 0);
|
||||
auto* ngs = reinterpret_cast<Ngs2Internal*>(system_handle);
|
||||
Common::LockGuard lock(ngs->mutex);
|
||||
std::vector<std::vector<float>> output_mix(num_buffer_info);
|
||||
std::vector<Ngs2VoiceInternal*> voices;
|
||||
{
|
||||
Common::LockGuard racks_lock(g_racks_mutex);
|
||||
@@ -1633,9 +1791,6 @@ int KYTY_SYSV_ABI Ngs2SystemRender(uintptr_t system_handle, const Ngs2RenderBuff
|
||||
}
|
||||
const auto grain = ngs->option.num_grain_samples;
|
||||
for (auto* voice: voices) {
|
||||
if (voice->channels == 0) {
|
||||
continue;
|
||||
}
|
||||
Ngs2RenderVoice(*voice, voices, grain);
|
||||
if (voice->rack->type != Ngs2RackType::Mastering || !voice->has_samples) {
|
||||
continue;
|
||||
@@ -1643,10 +1798,21 @@ int KYTY_SYSV_ABI Ngs2SystemRender(uintptr_t system_handle, const Ngs2RenderBuff
|
||||
EXIT_NOT_IMPLEMENTED(voice->output_id >= num_buffer_info);
|
||||
const auto& output = buffer_info[voice->output_id];
|
||||
EXIT_NOT_IMPLEMENTED(output.buffer == nullptr);
|
||||
EXIT_NOT_IMPLEMENTED(output.waveform_type != 0x18 ||
|
||||
EXIT_NOT_IMPLEMENTED((output.waveform_type != 0x12 && output.waveform_type != 0x18) ||
|
||||
output.num_channels != voice->channels);
|
||||
EXIT_NOT_IMPLEMENTED(output.buffer_size < grain * output.num_channels * sizeof(float));
|
||||
auto* pcm = static_cast<float*>(output.buffer);
|
||||
EXIT_NOT_IMPLEMENTED(output.buffer_size < grain * output.num_channels *
|
||||
(output.waveform_type == 0x12 ? sizeof(int16_t)
|
||||
: sizeof(float)));
|
||||
float* pcm = nullptr;
|
||||
if (output.waveform_type == 0x12) {
|
||||
auto& mix = output_mix[voice->output_id];
|
||||
if (mix.empty()) {
|
||||
mix.resize(grain * output.num_channels);
|
||||
}
|
||||
pcm = mix.data();
|
||||
} else {
|
||||
pcm = static_cast<float*>(output.buffer);
|
||||
}
|
||||
for (uint32_t dst = 0; dst < output.num_channels; ++dst) {
|
||||
for (uint32_t src = 0; src < voice->channels; ++src) {
|
||||
const float level = voice->output_matrix.empty()
|
||||
@@ -1658,6 +1824,18 @@ int KYTY_SYSV_ABI Ngs2SystemRender(uintptr_t system_handle, const Ngs2RenderBuff
|
||||
}
|
||||
}
|
||||
}
|
||||
for (uint32_t i = 0; i < num_buffer_info; ++i) {
|
||||
const auto& mix = output_mix[i];
|
||||
if (mix.empty()) {
|
||||
continue;
|
||||
}
|
||||
auto* pcm = static_cast<int16_t*>(buffer_info[i].buffer);
|
||||
for (size_t sample = 0; sample < mix.size(); ++sample) {
|
||||
pcm[sample] = static_cast<int16_t>(std::clamp(
|
||||
std::lrintf(std::clamp(mix[sample], -1.0f, 1.0f) * 32768.0f), -32768l,
|
||||
32767l));
|
||||
}
|
||||
}
|
||||
for (auto* voice: voices) {
|
||||
// No envelope is configured, so a release stop finishes in this render.
|
||||
if (voice->state == Ngs2VoicePlayState::Stopped) {
|
||||
@@ -1682,6 +1860,23 @@ static bool Ngs2FourCcEquals(const uint8_t* data, const char* four_cc) {
|
||||
return std::memcmp(data, four_cc, 4) == 0;
|
||||
}
|
||||
|
||||
static bool Ngs2GetAtrac9CodecInfo(std::array<uint8_t, ATRAC9_CONFIG_DATA_SIZE> config,
|
||||
Atrac9CodecInfo& codec) {
|
||||
if (config[0] != 0xfe || (config[1] & 1u) != 0 || ((config[1] >> 1u) & 7u) >= 6u) {
|
||||
return false;
|
||||
}
|
||||
void* decoder = Atrac9GetHandle();
|
||||
const bool valid = decoder != nullptr && Atrac9InitDecoder(decoder, config.data()) == 0 &&
|
||||
Atrac9GetCodecInfo(decoder, &codec) == 0 && codec.channels > 0 &&
|
||||
codec.samplingRate > 0 && codec.superframeSize > 0 &&
|
||||
codec.framesInSuperframe > 0 && codec.frameSamples > 0 &&
|
||||
codec.superframeSize % codec.framesInSuperframe == 0;
|
||||
if (decoder != nullptr) {
|
||||
Atrac9ReleaseHandle(decoder);
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
static int Ngs2ParseAtrac9Riff(const void* data, size_t data_size, Ngs2WaveformInfo* info) {
|
||||
static constexpr uint8_t ATRAC9_GUID[16] = {0xd2, 0x42, 0xe1, 0x47, 0xba, 0x36,
|
||||
0x8d, 0x4d, 0x88, 0xfc, 0x61, 0x65,
|
||||
@@ -1695,7 +1890,7 @@ static int Ngs2ParseAtrac9Riff(const void* data, size_t data_size, Ngs2WaveformI
|
||||
}
|
||||
|
||||
const uint64_t riff_end64 = 8ull + Ngs2ReadLe32(bytes + 4);
|
||||
if (riff_end64 < 12 || riff_end64 > data_size) {
|
||||
if (riff_end64 < 12) {
|
||||
return NGS2_ERROR_INVALID_WAVEFORM_DATA;
|
||||
}
|
||||
const auto riff_end = static_cast<size_t>(riff_end64);
|
||||
@@ -1705,11 +1900,13 @@ static int Ngs2ParseAtrac9Riff(const void* data, size_t data_size, Ngs2WaveformI
|
||||
size_t waveform_offset = 0;
|
||||
uint32_t waveform_size = 0;
|
||||
|
||||
for (size_t offset = 12; offset + 8 <= riff_end;) {
|
||||
for (size_t offset = 12; offset + 8 <= std::min(riff_end, data_size);) {
|
||||
const auto* chunk = bytes + offset;
|
||||
const auto chunk_size = static_cast<size_t>(Ngs2ReadLe32(chunk + 4));
|
||||
const auto payload = offset + 8;
|
||||
if (chunk_size > riff_end - payload) {
|
||||
const auto is_data = Ngs2FourCcEquals(chunk, "data");
|
||||
const uint64_t next = static_cast<uint64_t>(payload) + chunk_size + (chunk_size & 1u);
|
||||
if (next > riff_end || (next > data_size && !is_data)) {
|
||||
return NGS2_ERROR_INVALID_WAVEFORM_DATA;
|
||||
}
|
||||
|
||||
@@ -1723,7 +1920,7 @@ static int Ngs2ParseAtrac9Riff(const void* data, size_t data_size, Ngs2WaveformI
|
||||
return NGS2_ERROR_INVALID_WAVEFORM_FORMAT;
|
||||
}
|
||||
fact = bytes + payload;
|
||||
} else if (Ngs2FourCcEquals(chunk, "data")) {
|
||||
} else if (is_data) {
|
||||
if (payload > std::numeric_limits<uint32_t>::max()) {
|
||||
return NGS2_ERROR_INVALID_WAVEFORM_DATA;
|
||||
}
|
||||
@@ -1731,9 +1928,8 @@ static int Ngs2ParseAtrac9Riff(const void* data, size_t data_size, Ngs2WaveformI
|
||||
waveform_size = static_cast<uint32_t>(chunk_size);
|
||||
}
|
||||
|
||||
const uint64_t next = static_cast<uint64_t>(payload) + chunk_size + (chunk_size & 1u);
|
||||
if (next > riff_end) {
|
||||
return NGS2_ERROR_INVALID_WAVEFORM_DATA;
|
||||
if (next > data_size) {
|
||||
break;
|
||||
}
|
||||
offset = static_cast<size_t>(next);
|
||||
}
|
||||
@@ -1748,20 +1944,8 @@ static int Ngs2ParseAtrac9Riff(const void* data, size_t data_size, Ngs2WaveformI
|
||||
|
||||
std::array<uint8_t, ATRAC9_CONFIG_DATA_SIZE> config {};
|
||||
std::memcpy(config.data(), format + 44, config.size());
|
||||
if (config[0] != 0xfe || (config[1] & 1u) != 0 || ((config[1] >> 1u) & 7u) >= 6u) {
|
||||
return NGS2_ERROR_INVALID_WAVEFORM_FORMAT;
|
||||
}
|
||||
|
||||
Atrac9CodecInfo codec {};
|
||||
void* decoder = Atrac9GetHandle();
|
||||
const bool valid_codec =
|
||||
decoder != nullptr && Atrac9InitDecoder(decoder, config.data()) == 0 &&
|
||||
Atrac9GetCodecInfo(decoder, &codec) == 0 && codec.channels > 0 &&
|
||||
codec.samplingRate > 0 && codec.superframeSize > 0 && codec.framesInSuperframe > 0 &&
|
||||
codec.frameSamples > 0 && codec.superframeSize % codec.framesInSuperframe == 0;
|
||||
if (decoder != nullptr) {
|
||||
Atrac9ReleaseHandle(decoder);
|
||||
}
|
||||
const bool valid_codec = Ngs2GetAtrac9CodecInfo(config, codec);
|
||||
const uint64_t frame_samples =
|
||||
static_cast<uint64_t>(codec.frameSamples) * static_cast<uint64_t>(codec.framesInSuperframe);
|
||||
if (!valid_codec || codec.channels != Ngs2ReadLe16(format + 2) ||
|
||||
@@ -1819,6 +2003,32 @@ int KYTY_SYSV_ABI Ngs2CalcWaveformBlock(const Ngs2WaveformFormat* format, uint32
|
||||
EXIT_NOT_IMPLEMENTED(block == nullptr);
|
||||
|
||||
std::memset(block, 0, sizeof(Ngs2WaveformBlock));
|
||||
if (format == nullptr || format->num_channels == 0 || format->num_channels > 8 ||
|
||||
format->sample_rate == 0 || format->sample_rate > 192000) {
|
||||
return NGS2_ERROR_INVALID_WAVEFORM_FORMAT;
|
||||
}
|
||||
if (format->waveform_type == NGS2_WAVEFORM_TYPE_ATRAC9) {
|
||||
std::array<uint8_t, ATRAC9_CONFIG_DATA_SIZE> config {
|
||||
static_cast<uint8_t>(format->config_data >> 24u),
|
||||
static_cast<uint8_t>(format->config_data >> 16u),
|
||||
static_cast<uint8_t>(format->config_data >> 8u),
|
||||
static_cast<uint8_t>(format->config_data)};
|
||||
Atrac9CodecInfo codec {};
|
||||
if (!Ngs2GetAtrac9CodecInfo(config, codec) ||
|
||||
codec.channels != format->num_channels || codec.samplingRate != format->sample_rate) {
|
||||
return NGS2_ERROR_INVALID_WAVEFORM_FORMAT;
|
||||
}
|
||||
const uint64_t frame_samples = uint64_t(codec.frameSamples) * codec.framesInSuperframe;
|
||||
const uint64_t skip = sample_pos % frame_samples;
|
||||
block->data_offset = uint64_t(sample_pos / frame_samples) * codec.superframeSize;
|
||||
block->data_size = num_samples == 0
|
||||
? 0
|
||||
: ((skip + num_samples + frame_samples - 1) / frame_samples) *
|
||||
codec.superframeSize;
|
||||
block->num_skip_samples = num_samples == 0 ? 0 : static_cast<uint32_t>(skip);
|
||||
} else {
|
||||
return NGS2_ERROR_UNKNOWN_WAVEFORM_FORMAT;
|
||||
}
|
||||
block->num_samples = num_samples;
|
||||
return OK;
|
||||
}
|
||||
@@ -2076,7 +2286,21 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0x2000: EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Submixer); break;
|
||||
case 0x2000: {
|
||||
EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Submixer);
|
||||
if ((param->id & 0xffffu) == 0) {
|
||||
EXIT_NOT_IMPLEMENTED(param->size != sizeof(Ngs2SubmixerVoiceSetupParam));
|
||||
const auto& setup =
|
||||
*reinterpret_cast<const Ngs2SubmixerVoiceSetupParam*>(param);
|
||||
EXIT_NOT_IMPLEMENTED(
|
||||
setup.num_io_channels == 0 ||
|
||||
setup.num_io_channels > voice->rack->option.submixer.max_channels ||
|
||||
setup.flags != 0);
|
||||
voice->ResetSetupState();
|
||||
voice->channels = setup.num_io_channels;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0x2001: EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Reverb); break;
|
||||
case 0x3000: {
|
||||
EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Mastering);
|
||||
@@ -2145,18 +2369,32 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
|
||||
};
|
||||
const auto& blocks = *reinterpret_cast<const BlocksParam*>(param);
|
||||
EXIT_NOT_IMPLEMENTED(!voice->accepts_blocks ||
|
||||
(blocks.flags != 0 && blocks.flags != 1 &&
|
||||
blocks.flags != 0x11 &&
|
||||
blocks.flags != 0x4));
|
||||
if (blocks.flags == 0x4) {
|
||||
(blocks.flags & ~0x17u) != 0);
|
||||
if ((blocks.flags & 0x4u) != 0) {
|
||||
voice->blocks.clear();
|
||||
voice->sample_phase = 0;
|
||||
voice->waveform_end = nullptr;
|
||||
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) {
|
||||
const auto& block = blocks.blocks[i];
|
||||
EXIT_NOT_IMPLEMENTED(block.num_samples == 0);
|
||||
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 ||
|
||||
voice->decoder != nullptr));
|
||||
@@ -2179,12 +2417,12 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 5: {
|
||||
const float ratio = *reinterpret_cast<const float*>(param + 1);
|
||||
EXIT_NOT_IMPLEMENTED(!std::isfinite(ratio) || ratio < 0.0f || ratio > 4.0f ||
|
||||
(voice->decoder != nullptr && ratio != 1.0f));
|
||||
case 5: {
|
||||
const float ratio = *reinterpret_cast<const float*>(param + 1);
|
||||
EXIT_NOT_IMPLEMENTED(!std::isfinite(ratio));
|
||||
const float clamped_ratio = std::clamp(ratio, 0.0f, 4.0f);
|
||||
voice->sample_step = static_cast<uint64_t>(std::llround(
|
||||
static_cast<double>(voice->sample_rate) * ratio * 4294967296.0));
|
||||
static_cast<double>(voice->sample_rate) * clamped_ratio * 4294967296.0));
|
||||
break;
|
||||
}
|
||||
case 0xa: {
|
||||
|
||||
+263
-7
@@ -88,6 +88,194 @@ void TestStreamingResample() {
|
||||
"streaming progress does not match consumed source data");
|
||||
}
|
||||
|
||||
void TestAtrac9ResampleAcrossFrames() {
|
||||
const auto make_voice = [] {
|
||||
auto f = std::make_unique<Fixture>(24000);
|
||||
f->voice.SetupSampler({NGS2_WAVEFORM_TYPE_ATRAC9, 1, 24000, 0xfe4005f0});
|
||||
f->voice.state = Ngs2VoicePlayState::Playing;
|
||||
f->voice.decoded_frame[0] = 0.0f;
|
||||
f->voice.decoded_frame[1] = 0.5f;
|
||||
f->voice.frame_samples = 2;
|
||||
f->voice.next_decoded_frame.resize(f->voice.decoded_frame.size());
|
||||
f->voice.next_decoded_frame[0] = 1.0f;
|
||||
f->voice.next_decoded_frame[1] = 1.5f;
|
||||
f->voice.next_frame_samples = 2;
|
||||
f->voice.accepts_blocks = false;
|
||||
return f;
|
||||
};
|
||||
auto whole = make_voice();
|
||||
whole->Render(8);
|
||||
const std::vector<float> expected {0.0f, 0.25f, 0.5f, 0.75f,
|
||||
1.0f, 1.25f, 1.5f, 1.5f};
|
||||
Check(whole->voice.samples == expected && whole->voice.decoded_samples == 4 &&
|
||||
whole->voice.state == Ngs2VoicePlayState::Empty,
|
||||
"24 kHz ATRAC9 interpolation or source progress failed at frame boundary");
|
||||
auto split = make_voice();
|
||||
split->Render(3);
|
||||
const auto first = split->voice.samples;
|
||||
split->Render(5);
|
||||
std::vector<float> joined = first;
|
||||
joined.insert(joined.end(), split->voice.samples.begin(), split->voice.samples.end());
|
||||
Check(joined == expected && split->voice.decoded_samples == 4,
|
||||
"ATRAC9 resampling changed across render grains");
|
||||
auto pitched = make_voice();
|
||||
struct Pitch {
|
||||
Ngs2VoiceParamHeader header {16, 0, 0x10000005};
|
||||
float ratio = 2.0f;
|
||||
uint32_t reserved = 0;
|
||||
} pitch;
|
||||
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&pitched->voice), &pitch.header) == OK,
|
||||
"ATRAC9 pitch control failed");
|
||||
pitched->Render(4);
|
||||
Check(pitched->voice.samples == std::vector<float> {0.0f, 0.5f, 1.0f, 1.5f} &&
|
||||
pitched->voice.decoded_samples == 4,
|
||||
"ATRAC9 pitch did not use source-rate phase advancement");
|
||||
}
|
||||
|
||||
void TestAtrac9ResampleAfterStarvation() {
|
||||
Fixture f(24000);
|
||||
f.voice.SetupSampler({NGS2_WAVEFORM_TYPE_ATRAC9, 1, 24000, 0xfe4005f0});
|
||||
f.voice.state = Ngs2VoicePlayState::Playing;
|
||||
f.voice.accepts_blocks = true;
|
||||
f.voice.decoded_frame[0] = 0.0f;
|
||||
f.voice.decoded_frame[1] = 0.5f;
|
||||
f.voice.frame_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,
|
||||
"ATRAC9 resampling lost the final sample before starvation");
|
||||
f.Render(2);
|
||||
Check(!f.voice.has_samples && f.voice.state == Ngs2VoicePlayState::Playing &&
|
||||
f.voice.decoded_samples == 2,
|
||||
"starved ATRAC9 stream consumed source samples");
|
||||
f.voice.next_decoded_frame.resize(f.voice.decoded_frame.size());
|
||||
f.voice.next_decoded_frame[0] = 1.0f;
|
||||
f.voice.next_decoded_frame[1] = 1.5f;
|
||||
f.voice.next_frame_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,
|
||||
"ATRAC9 refill lost source-rate phase");
|
||||
}
|
||||
|
||||
void TestWaveformBlockGeometry() {
|
||||
const Ngs2WaveformFormat format {NGS2_WAVEFORM_TYPE_ATRAC9, 1, 24000, 0xfe4005f0};
|
||||
Ngs2WaveformBlock block {};
|
||||
Check(Ngs2CalcWaveformBlock(&format, 0, 34534, &block) == OK &&
|
||||
block.data_offset == 0 && block.data_size == 13056 &&
|
||||
block.num_skip_samples == 0 && block.num_samples == 34534,
|
||||
"ATRAC9 block calculation omitted encoded superframes");
|
||||
Check(Ngs2CalcWaveformBlock(&format, 513, 1, &block) == OK &&
|
||||
block.data_offset == 192 && block.data_size == 192 &&
|
||||
block.num_skip_samples == 1 && block.num_samples == 1,
|
||||
"ATRAC9 block calculation lost the source offset");
|
||||
Check(Ngs2CalcWaveformBlock(&format, 0, 0, &block) == OK && block.data_size == 0 &&
|
||||
block.num_samples == 0,
|
||||
"empty ATRAC9 block acquired encoded data");
|
||||
Fixture f(24000);
|
||||
const int16_t silence = 0;
|
||||
f.Queue(&silence, 0, 0, UINT32_MAX);
|
||||
f.Render(1);
|
||||
Check(f.voice.blocks.empty() && f.voice.state == Ngs2VoicePlayState::Empty,
|
||||
"empty repeated block kept the sampler active");
|
||||
}
|
||||
|
||||
void TestSubmixerSetupAndRouting() {
|
||||
Fixture f(48000, 2);
|
||||
Ngs2RackInternal submixer_rack {};
|
||||
submixer_rack.ngs = &f.system;
|
||||
submixer_rack.type = Ngs2RackType::Submixer;
|
||||
submixer_rack.option.submixer.max_channels = 8;
|
||||
Ngs2VoiceInternal submixer;
|
||||
submixer.rack = &submixer_rack;
|
||||
submixer.ports.resize(1);
|
||||
submixer.matrices.resize(1);
|
||||
Ngs2RackInternal mastering_rack {};
|
||||
mastering_rack.type = Ngs2RackType::Mastering;
|
||||
Ngs2VoiceInternal mastering;
|
||||
mastering.rack = &mastering_rack;
|
||||
mastering.channels = 2;
|
||||
mastering.state = Ngs2VoicePlayState::Playing;
|
||||
submixer.state = Ngs2VoicePlayState::Playing;
|
||||
submixer.ports[0].dest = &mastering;
|
||||
submixer.matrices[0] = {1.0f};
|
||||
f.voice.ports.push_back({&submixer, 0, 1.0f, -1});
|
||||
const int16_t pcm[] = {8192, -8192, 16384, -16384};
|
||||
f.Queue(pcm, 2, 0);
|
||||
const std::vector<Ngs2VoiceInternal*> voices {&f.voice, &submixer, &mastering};
|
||||
Ngs2RenderVoice(mastering, voices, 1);
|
||||
Check(!submixer.has_samples && !mastering.has_samples && f.voice.decoded_samples == 0,
|
||||
"unconfigured zero-channel submixer claimed to produce samples");
|
||||
Ngs2SubmixerVoiceSetupParam setup {{16, 0, 0x20000000}, 2, 0};
|
||||
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&submixer), &setup.header) == OK &&
|
||||
submixer.channels == 2 && submixer.state == Ngs2VoicePlayState::Empty &&
|
||||
submixer.ports[0].dest == nullptr && submixer.matrices[0].empty(),
|
||||
"submixer setup did not establish channels and clear old routing");
|
||||
submixer.state = Ngs2VoicePlayState::Playing;
|
||||
submixer.ports[0] = {&mastering, 0, 1.0f, 0};
|
||||
submixer.matrices[0] = {1.0f, 0.0f, 0.0f, 1.0f};
|
||||
for (float expected: {0.25f, 0.5f}) {
|
||||
for (auto* voice: voices) {
|
||||
voice->rendered = false;
|
||||
}
|
||||
Ngs2RenderVoice(mastering, voices, 1);
|
||||
Check(mastering.has_samples && mastering.samples[0] == expected &&
|
||||
mastering.samples[1] == -expected,
|
||||
"submixer lost routed samples across render grains");
|
||||
}
|
||||
}
|
||||
|
||||
void TestPcm16RenderOutput() {
|
||||
Ngs2Internal system;
|
||||
system.option = Ngs2DefaultSystemOption();
|
||||
system.option.num_grain_samples = 4;
|
||||
Ngs2RackOptionUnion sampler_option {}, mastering_option {};
|
||||
Ngs2FillDefaultRackOption(0x1000, &sampler_option);
|
||||
Ngs2FillDefaultRackOption(0x3000, &mastering_option);
|
||||
sampler_option.common.max_voices = 2;
|
||||
sampler_option.common.max_ports = 1;
|
||||
alignas(Ngs2RackInternal) alignas(Ngs2VoiceInternal)
|
||||
std::byte sampler_storage[sizeof(Ngs2RackInternal) + 2 * sizeof(Ngs2VoiceInternal)];
|
||||
alignas(Ngs2RackInternal) alignas(Ngs2VoiceInternal)
|
||||
std::byte mastering_storage[sizeof(Ngs2RackInternal) + sizeof(Ngs2VoiceInternal)];
|
||||
Ngs2ContextBufferInfo sampler_buffer {sampler_storage, sizeof(sampler_storage)};
|
||||
Ngs2ContextBufferInfo mastering_buffer {mastering_storage, sizeof(mastering_storage)};
|
||||
uintptr_t sampler_handle = 0, mastering_handle = 0;
|
||||
const auto system_handle = reinterpret_cast<uintptr_t>(&system);
|
||||
Check(Ngs2RackCreate(system_handle, 0x1000, &sampler_option.common, &sampler_buffer,
|
||||
&sampler_handle) == OK &&
|
||||
Ngs2RackCreate(system_handle, 0x3000, &mastering_option.common,
|
||||
&mastering_buffer, &mastering_handle) == OK,
|
||||
"PCM16 render rack creation failed");
|
||||
auto* samplers = reinterpret_cast<Ngs2VoiceInternal*>(
|
||||
reinterpret_cast<Ngs2RackInternal*>(sampler_handle) + 1);
|
||||
auto& mastering = *reinterpret_cast<Ngs2VoiceInternal*>(
|
||||
reinterpret_cast<Ngs2RackInternal*>(mastering_handle) + 1);
|
||||
mastering.channels = 1;
|
||||
mastering.state = Ngs2VoicePlayState::Playing;
|
||||
const int16_t source[2][4] = {{16384, -16384, 32767, -32768},
|
||||
{8192, -8192, 32767, -32768}};
|
||||
for (uint32_t i = 0; i < 2; ++i) {
|
||||
samplers[i].SetupSampler({0x12, 1, 48000});
|
||||
samplers[i].blocks.push_back(
|
||||
{reinterpret_cast<const uint8_t*>(source[i]), {0, sizeof(source[i]), 0, 0, 4}});
|
||||
samplers[i].ports[0].dest = &mastering;
|
||||
samplers[i].SetEvent(1);
|
||||
}
|
||||
int16_t output_samples[4] = {};
|
||||
Ngs2RenderBufferInfo output {output_samples, sizeof(output_samples), 0x12, 1};
|
||||
Check(Ngs2SystemRender(system_handle, &output, 1) == OK &&
|
||||
output_samples[0] == 24576 && output_samples[1] == -24576 &&
|
||||
output_samples[2] == 32767 && output_samples[3] == -32768,
|
||||
"PCM16 output failed to mix or saturate mastering voices");
|
||||
Check(Ngs2SystemRender(system_handle, &output, 1) == OK &&
|
||||
std::ranges::all_of(output_samples, [](int16_t sample) { return sample == 0; }),
|
||||
"PCM16 output retained samples from the previous render");
|
||||
Check(Ngs2RackDestroy(mastering_handle, nullptr) == OK &&
|
||||
Ngs2RackDestroy(sampler_handle, nullptr) == OK,
|
||||
"PCM16 render rack cleanup failed");
|
||||
}
|
||||
|
||||
void TestPitchLoopAndSkip() {
|
||||
Fixture f(24000);
|
||||
const int16_t pcm[] = {-30000, 8192, 16384};
|
||||
@@ -106,6 +294,22 @@ void TestPitchLoopAndSkip() {
|
||||
}
|
||||
}
|
||||
|
||||
void TestPitchClamps() {
|
||||
Fixture f(48000);
|
||||
const int16_t pcm[] = {8192, 0, 0, 0, -8192};
|
||||
f.Queue(pcm, 5, 0);
|
||||
struct Pitch {
|
||||
Ngs2VoiceParamHeader header {16, 0, 0x10000005};
|
||||
float ratio = 5.0f;
|
||||
uint32_t reserved = 0;
|
||||
} pitch;
|
||||
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&f.voice), &pitch.header) == OK,
|
||||
"out-of-range pitch was rejected");
|
||||
f.Render(2);
|
||||
Check(f.voice.samples[0] == 0.25f && f.voice.samples[1] == -0.25f,
|
||||
"pitch was not clamped to four times playback speed");
|
||||
}
|
||||
|
||||
void TestStarvationAndPause() {
|
||||
Fixture f(48000);
|
||||
const int16_t pcm[] = {16384, 8192};
|
||||
@@ -149,6 +353,27 @@ void TestMonoRateAndRouting() {
|
||||
"22.05 kHz effect duration was incorrect");
|
||||
}
|
||||
|
||||
void TestMatrixOutputChannels() {
|
||||
Fixture f(48000, 2);
|
||||
Ngs2RackInternal mastering_rack {};
|
||||
mastering_rack.type = Ngs2RackType::Mastering;
|
||||
Ngs2VoiceInternal mastering;
|
||||
mastering.rack = &mastering_rack;
|
||||
mastering.channels = 8;
|
||||
mastering.state = Ngs2VoicePlayState::Playing;
|
||||
f.voice.matrices = {{1.0f, 0.0f, 0.0f, 1.0f}};
|
||||
f.voice.ports.push_back({&mastering, 0, 1.0f, 0});
|
||||
const int16_t pcm[] = {8192, -8192};
|
||||
f.Queue(pcm, 1, 0);
|
||||
Ngs2RenderVoice(mastering, {&f.voice, &mastering}, 1);
|
||||
Check(mastering.samples[0] == 0.25f && mastering.samples[1] == -0.25f,
|
||||
"stereo matrix did not route its output channels");
|
||||
for (size_t channel = 2; channel < mastering.channels; ++channel) {
|
||||
Check(mastering.samples[channel] == 0.0f,
|
||||
"matrix routed audio to an unspecified output channel");
|
||||
}
|
||||
}
|
||||
|
||||
void TestCustomPcmStillPlays() {
|
||||
Fixture f(48000, 1, Ngs2RackType::CustomSampler);
|
||||
const int16_t pcm[] = {8192, -16384};
|
||||
@@ -190,6 +415,29 @@ void TestSamplerReuseResetsRouting() {
|
||||
}
|
||||
}
|
||||
|
||||
void TestDeferredSamplerSetup() {
|
||||
Fixture f(48000);
|
||||
struct Setup {
|
||||
Ngs2VoiceParamHeader header {40, 0, 0x10000000};
|
||||
Ngs2WaveformFormat format;
|
||||
uint32_t flags = 0, reserved = 0;
|
||||
} setup;
|
||||
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&f.voice), &setup.header) == OK,
|
||||
"deferred stream setup failed");
|
||||
Check(f.voice.state == Ngs2VoicePlayState::Empty && f.voice.channels == 0 &&
|
||||
f.voice.sample_rate == 0 && f.voice.decoder == nullptr,
|
||||
"deferred stream setup did not clear the sampler");
|
||||
setup.format = {0x12, 1, 48000};
|
||||
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&f.voice), &setup.header) == OK,
|
||||
"sampler setup after deferred stream failed");
|
||||
const int16_t pcm = 8192;
|
||||
f.Queue(&pcm, 1, 0);
|
||||
f.voice.SetEvent(1);
|
||||
f.Render(1);
|
||||
Check(f.voice.samples[0] == 0.25f,
|
||||
"sampler did not play after deferred stream setup");
|
||||
}
|
||||
|
||||
void TestPlayStateBeforeRender() {
|
||||
Fixture f(48000);
|
||||
const int16_t pcm[] = {8192, -16384};
|
||||
@@ -270,7 +518,7 @@ void TestStopThenPlayBeforeRender() {
|
||||
void SetLowPass(Fixture& f, float frequency = 1000.0f, uint64_t mask = 0) {
|
||||
struct Param {
|
||||
Ngs2VoiceParamHeader header {56, 0, 0x1000000a};
|
||||
Ngs2SamplerFilterParam filter {0, 1, 0, 1, 1000.0f, 0.70710678f, 1.0f, {}};
|
||||
Ngs2SamplerFilterParam filter {0, 1, 1, 0, 1000.0f, 0.70710678f, 1.0f, {}};
|
||||
} param;
|
||||
param.filter.frequency = frequency;
|
||||
param.filter.channel_mask = mask;
|
||||
@@ -366,8 +614,8 @@ void TestFilterControlLayout() {
|
||||
Fixture f(48000, 2);
|
||||
f.rack.option.sampler.max_filters = 8;
|
||||
// A 56-byte control command with nonzero trailing padding.
|
||||
alignas(8) uint32_t words[] = {56, 0x1000000a, 0, 1, 0, 0, 1,
|
||||
0x45d58800, 0x3fd55555, 0x3f800000, 0, 0, 0, 0x13f};
|
||||
alignas(8) uint32_t words[] = {56, 0x1000000a, 0, 1, 1, 0, 0,
|
||||
0, 0x45d58800, 0x3fd55555, 0x3f800000, 0, 0, 0x13f};
|
||||
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&f.voice),
|
||||
reinterpret_cast<const Ngs2VoiceParamHeader*>(words));
|
||||
Check(f.voice.filters.size() == 1 && f.voice.filters[0].enabled,
|
||||
@@ -377,8 +625,8 @@ void TestFilterControlLayout() {
|
||||
const double expected = (1 - std::cos(omega)) / (2 * (1 + std::sin(omega) / (2 * q)));
|
||||
Check(std::abs(f.voice.filters[0].b0 - expected) < 1e-10,
|
||||
"FCQ fields were read at the wrong offsets");
|
||||
words[4] = 1; // Bypass channel 0 in the low half of the 64-bit mask.
|
||||
words[5] = 1; // Bypass channel 32 in the high half.
|
||||
words[6] = 1; // Bypass channel 0 in the low half of the 64-bit mask.
|
||||
words[7] = 1; // Bypass channel 32 in the high half.
|
||||
Fixture wide(48000, 33);
|
||||
wide.rack.option.sampler.max_filters = 8;
|
||||
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&wide.voice),
|
||||
@@ -389,8 +637,8 @@ void TestFilterControlLayout() {
|
||||
Check(wide.voice.samples[0] == 0.5f && wide.voice.samples[32] == 0.5f &&
|
||||
std::abs(wide.voice.samples[1] - expected * 0.5) < 1e-7,
|
||||
"channel mask was not decoded as a 64-bit bypass mask");
|
||||
words[6] = 0; // OFF ignores unused FCQ values and clears the old delay line.
|
||||
words[7] = 0x7fc00000;
|
||||
words[4] = 0; // OFF ignores unused FCQ values and clears the old delay line.
|
||||
words[8] = 0x7fc00000;
|
||||
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&wide.voice),
|
||||
reinterpret_cast<const Ngs2VoiceParamHeader*>(words));
|
||||
Check(!wide.voice.filters[0].enabled && wide.voice.filters[0].history.empty() &&
|
||||
@@ -664,8 +912,14 @@ void TestFiniteFilterTail() {
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
TestPcm16RenderOutput();
|
||||
TestSubmixerSetupAndRouting();
|
||||
TestWaveformBlockGeometry();
|
||||
TestAtrac9ResampleAfterStarvation();
|
||||
TestAtrac9ResampleAcrossFrames();
|
||||
TestResamplingPhaseAfterStarvation();
|
||||
TestSamplerReuseResetsRouting();
|
||||
TestDeferredSamplerSetup();
|
||||
TestLoopCallbacksAndExit();
|
||||
TestWaveformReadAddress();
|
||||
TestFilterTailDuringStarvation();
|
||||
@@ -679,8 +933,10 @@ int main() {
|
||||
TestCallbackPauseDuringLargeStep();
|
||||
TestStreamingResample();
|
||||
TestPitchLoopAndSkip();
|
||||
TestPitchClamps();
|
||||
TestStarvationAndPause();
|
||||
TestMonoRateAndRouting();
|
||||
TestMatrixOutputChannels();
|
||||
TestCustomPcmStillPlays();
|
||||
TestPlayStateBeforeRender();
|
||||
TestStatePublication();
|
||||
|
||||
Reference in New Issue
Block a user