mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-26 12:56:54 +01:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e1c2d1532 | |||
| 0ec43a8d3d | |||
| bfdbba3aa4 | |||
| f7ae6e0a83 |
@@ -465,6 +465,13 @@ add_executable(audio_out2_port_tests EXCLUDE_FROM_ALL
|
||||
target_link_libraries(audio_out2_port_tests common fmt::fmt)
|
||||
target_include_directories(audio_out2_port_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(ngs2_sampler_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/Ngs2SamplerTests.cpp"
|
||||
"${KYTY_SOURCE_DIR}/loader/timer.cpp"
|
||||
)
|
||||
target_link_libraries(ngs2_sampler_tests common fmt::fmt LibAtrac9)
|
||||
target_include_directories(ngs2_sampler_tests PRIVATE ${inc_headers})
|
||||
|
||||
add_executable(save_data_memory_tests EXCLUDE_FROM_ALL
|
||||
"${KYTY_TESTS_DIR}/SaveDataMemoryTests.cpp"
|
||||
)
|
||||
@@ -555,6 +562,7 @@ if(BUILD_TESTING)
|
||||
add_test(NAME kernel_file_system COMMAND $<TARGET_FILE:kernel_file_system_tests>)
|
||||
add_test(NAME sync_on_address COMMAND $<TARGET_FILE:sync_on_address_tests>)
|
||||
add_test(NAME audio_out2_port COMMAND $<TARGET_FILE:audio_out2_port_tests>)
|
||||
add_test(NAME ngs2_sampler COMMAND $<TARGET_FILE:ngs2_sampler_tests>)
|
||||
add_test(NAME save_data_memory COMMAND $<TARGET_FILE:save_data_memory_tests>)
|
||||
add_test(NAME shader_recompiler_compute COMMAND $<TARGET_FILE:shader_recompiler_compute_tests>)
|
||||
add_test(NAME shader_recompiler_alignbyte
|
||||
@@ -620,6 +628,7 @@ if(BUILD_TESTING)
|
||||
shader_recompiler_compute_tests
|
||||
virtual_memory_allocation_tests
|
||||
audio_out2_port_tests
|
||||
ngs2_sampler_tests
|
||||
)
|
||||
endif()
|
||||
|
||||
|
||||
@@ -1205,8 +1205,9 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
||||
}
|
||||
break;
|
||||
case 0x02:
|
||||
case 0x04:
|
||||
if constexpr (sizeof(T) == sizeof(uint32_t)) {
|
||||
if (eop_event_type == 0x2f && event_index == 0x06) {
|
||||
if (event_write_source == 0x02 && eop_event_type == 0x2f && event_index == 0x06) {
|
||||
switch (cache_action) {
|
||||
case 0x00: write32(false); return;
|
||||
case 0x38: write32(true); return;
|
||||
@@ -1214,6 +1215,9 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (event_write_source == 0x04) {
|
||||
value = Sync::ReadReferenceClock();
|
||||
}
|
||||
auto write64 = [&](bool with_writeback) {
|
||||
auto* dst = static_cast<uint64_t*>(dst_gpu_addr);
|
||||
std::memcpy(dst, &value, sizeof(value));
|
||||
@@ -1270,7 +1274,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
||||
case 0x14:
|
||||
case 0x28:
|
||||
if (((eop_event_type == 0x04 || eop_event_type == 0x28) &&
|
||||
event_index == 0x05 && !with_interrupt) ||
|
||||
event_index == 0x05) ||
|
||||
(event_index == 0x00)) {
|
||||
write64(true);
|
||||
return;
|
||||
@@ -1302,45 +1306,6 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 0x04:
|
||||
if constexpr (sizeof(T) == sizeof(uint64_t)) {
|
||||
const auto clock = Sync::ReadReferenceClock();
|
||||
auto* dst = static_cast<uint64_t*>(dst_gpu_addr);
|
||||
std::memcpy(dst, &clock, sizeof(clock));
|
||||
switch (cache_action) {
|
||||
case 0x00:
|
||||
if ((eop_event_type == 0x04 && event_index == 0x05) ||
|
||||
(eop_event_type == 0x28 && event_index == 0x00)) {
|
||||
if (with_interrupt) {
|
||||
Sync::WriteAtEndOfPipeWithInterrupt64(
|
||||
m_submit_id, command, dst, clock, m_interrupt_event_id,
|
||||
interrupt_context_id);
|
||||
} else {
|
||||
Sync::WriteAtEndOfPipeClockCounter(m_submit_id, command,
|
||||
dst, clock);
|
||||
}
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 0x38:
|
||||
if ((eop_event_type == 0x04 &&
|
||||
(event_index == 0x00 || event_index == 0x05)) ||
|
||||
(eop_event_type == 0x28 && event_index == 0x00)) {
|
||||
if (with_interrupt) {
|
||||
Sync::WriteAtEndOfPipeWithInterruptWriteBack64(
|
||||
m_submit_id, command, dst, clock, m_interrupt_event_id,
|
||||
interrupt_context_id);
|
||||
} else {
|
||||
Sync::WriteAtEndOfPipeClockCounterWithWriteBack(
|
||||
m_submit_id, command, dst, clock);
|
||||
}
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
|
||||
+3
-39
@@ -107,36 +107,6 @@ constexpr uint64_t NumFramesBeforeRemoval = 32;
|
||||
return true;
|
||||
}
|
||||
|
||||
[[nodiscard]] const char* BindingTypeName(TextureCache::BindingType type) {
|
||||
switch (type) {
|
||||
case TextureCache::BindingType::Texture: return "Texture";
|
||||
case TextureCache::BindingType::Storage: return "StorageTexture";
|
||||
case TextureCache::BindingType::RenderTarget: return "ColorTarget";
|
||||
case TextureCache::BindingType::DepthTarget: return "DepthTarget";
|
||||
case TextureCache::BindingType::VideoOut: return "VideoOut";
|
||||
}
|
||||
return "Image";
|
||||
}
|
||||
|
||||
void NameImageBinding(GraphicContext& graphics, Image& image, vk::ImageView view,
|
||||
TextureCache::BindingType type, const ImageViewInfo& view_info) {
|
||||
const auto* role = BindingTypeName(type);
|
||||
SetVulkanObjectNameF(
|
||||
graphics.device, image.backing.image,
|
||||
"Kyty.{}.Image[guest=0x{:016x} size=0x{:x} extent={}x{}x{} format={} mips={} layers={} "
|
||||
"samples={}]",
|
||||
role, image.info.data.address, image.info.data.size, image.info.extent.width,
|
||||
image.info.extent.height, image.info.extent.depth,
|
||||
static_cast<uint32_t>(image.info.pixel_format), image.info.resources.levels,
|
||||
image.info.resources.layers, image.info.samples);
|
||||
SetVulkanObjectNameF(
|
||||
graphics.device, view,
|
||||
"Kyty.{}.View[guest=0x{:016x} format={} aspect=0x{:x} mip={}+{} layer={}+{}]", role,
|
||||
image.info.data.address, static_cast<uint32_t>(view_info.format),
|
||||
static_cast<vk::ImageAspectFlags::MaskType>(view_info.aspect), view_info.base_level,
|
||||
view_info.level_count, view_info.base_layer, view_info.layer_count);
|
||||
}
|
||||
|
||||
[[nodiscard]] std::vector<vk::BufferImageCopy> BuildDepthCopies(const ImageInfo& info,
|
||||
uint64_t slice_stride,
|
||||
vk::ImageAspectFlags aspect) {
|
||||
@@ -1443,9 +1413,7 @@ vk::ImageView TextureCache::FindTexture(ImageId id, const ImageDesc& desc) {
|
||||
break;
|
||||
default: EXIT("TextureCache: invalid texture binding\n");
|
||||
}
|
||||
const auto view = image.FindView(desc.view_info);
|
||||
NameImageBinding(m_graphics, image, view, desc.type, desc.view_info);
|
||||
return view;
|
||||
return image.FindView(desc.view_info);
|
||||
}
|
||||
|
||||
vk::ImageView TextureCache::FindRenderTarget(ImageId id, const ImageDesc& desc) {
|
||||
@@ -1463,9 +1431,7 @@ vk::ImageView TextureCache::FindRenderTarget(ImageId id, const ImageDesc& desc)
|
||||
RefreshImage(id);
|
||||
CommitGpuWrite(image);
|
||||
TrackImageDownload(id, image);
|
||||
const auto view = image.FindView(desc.view_info);
|
||||
NameImageBinding(m_graphics, image, view, desc.type, desc.view_info);
|
||||
return view;
|
||||
return image.FindView(desc.view_info);
|
||||
}
|
||||
|
||||
vk::ImageView TextureCache::FindDepthTarget(ImageId id, const ImageDesc& desc) {
|
||||
@@ -1492,9 +1458,7 @@ vk::ImageView TextureCache::FindDepthTarget(ImageId id, const ImageDesc& desc) {
|
||||
if (desc.info.HasStencil()) {
|
||||
RefreshImage(AssociateStencil(id, desc.info.stencil));
|
||||
}
|
||||
const auto view = image.FindView(desc.view_info);
|
||||
NameImageBinding(m_graphics, image, view, desc.type, desc.view_info);
|
||||
return view;
|
||||
return image.FindView(desc.view_info);
|
||||
}
|
||||
|
||||
void TextureCache::MarkGpuWritten(ImageId id) {
|
||||
|
||||
@@ -690,6 +690,12 @@ Image::Image(GraphicContext& graphics, CommandScheduler& scheduler, const ImageI
|
||||
create.extent.width, create.extent.height, create.extent.depth,
|
||||
static_cast<int>(create.format), create.arrayLayers, create.mipLevels);
|
||||
}
|
||||
SetVulkanObjectNameF(
|
||||
graphics.device, backing.image,
|
||||
"Kyty.Image[guest=0x{:016x} size=0x{:x} extent={}x{}x{} format={} mips={} layers={} samples={}]",
|
||||
info.data.address, info.data.size, info.extent.width, info.extent.height, info.extent.depth,
|
||||
static_cast<uint32_t>(info.pixel_format), info.resources.levels, info.resources.layers,
|
||||
info.samples);
|
||||
}
|
||||
|
||||
uint64_t Image::HashGuestEdges() const {
|
||||
|
||||
@@ -391,6 +391,12 @@ vk::ImageView Image::FindView(const ImageViewInfo& view_info) {
|
||||
view_info.level_count, view_info.base_layer, view_info.layer_count,
|
||||
static_cast<vk::ImageUsageFlags::MaskType>(view_info.usage));
|
||||
}
|
||||
SetVulkanObjectNameF(
|
||||
m_graphics.device, view,
|
||||
"Kyty.ImageView[guest=0x{:016x} format={} aspect=0x{:x} mip={}+{} layer={}+{}]",
|
||||
info.data.address, static_cast<uint32_t>(normalized.format),
|
||||
static_cast<vk::ImageAspectFlags::MaskType>(normalized.aspect), normalized.base_level,
|
||||
normalized.level_count, normalized.base_layer, normalized.layer_count);
|
||||
views.push_back({normalized, view});
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -473,15 +473,6 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
|
||||
}
|
||||
const auto image_view = cache.FindRenderTarget(target.image_id, target.desc);
|
||||
auto& image = cache.GetImage(target.image_id);
|
||||
SetVulkanObjectNameF(m_context.GetGraphics().device, image.backing.image,
|
||||
"Kyty.MRT{}.Image[guest=0x{:016x} size=0x{:x} format={}]",
|
||||
target.target_slot, image.info.data.address, image.info.data.size,
|
||||
static_cast<uint32_t>(image.info.pixel_format));
|
||||
SetVulkanObjectNameF(m_context.GetGraphics().device, image_view,
|
||||
"Kyty.MRT{}.View[guest=0x{:016x} mip={} layer={}+{}]",
|
||||
target.target_slot, image.info.data.address,
|
||||
target.desc.view_info.base_level, target.desc.view_info.base_layer,
|
||||
target.desc.view_info.layer_count);
|
||||
EXIT_IF(image.backing.samples != target.desc.info.samples || image_view == nullptr);
|
||||
const auto& view = target.desc.view_info;
|
||||
const auto layout = image.binding.is_bound ? vk::ImageLayout::eGeneral
|
||||
@@ -522,14 +513,6 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
|
||||
EXIT("failed to consume HTile clear state\n");
|
||||
}
|
||||
auto& image = cache.GetImage(depth.image_id);
|
||||
SetVulkanObjectNameF(m_context.GetGraphics().device, image.backing.image,
|
||||
"Kyty.DepthTarget.Image[guest=0x{:016x} size=0x{:x} format={}]",
|
||||
image.info.data.address, image.info.data.size,
|
||||
static_cast<uint32_t>(image.info.pixel_format));
|
||||
SetVulkanObjectNameF(m_context.GetGraphics().device, image_view,
|
||||
"Kyty.DepthTarget.View[guest=0x{:016x} layer={}+{}]",
|
||||
image.info.data.address, depth.desc.view_info.base_layer,
|
||||
depth.desc.view_info.layer_count);
|
||||
EXIT_IF(image_view == nullptr || image.backing.samples != depth.desc.info.samples);
|
||||
const auto draw_writes = depth.AttachmentWriteAspects();
|
||||
vk::ImageAspectFlags sampled_aspects;
|
||||
|
||||
@@ -112,26 +112,6 @@ void WriteAtEndOfPipe64(uint64_t submit_id, CommandBuffer& buffer, uint64_t* dst
|
||||
EndOfPipeWriteSize::Qword, EndOfPipeWriteAction::Write);
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipeClockCounter(uint64_t submit_id, CommandBuffer& buffer, uint64_t* dst_gpu_addr,
|
||||
uint64_t value) {
|
||||
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), 0,
|
||||
EndOfPipeWriteSize::Qword, EndOfPipeWriteAction::Write);
|
||||
|
||||
LOGF_COLOR(Log::Color::BrightGreen,
|
||||
"EndOfPipe Signal!!! [0x%016" PRIx64 "] <- Clock: 0x%016" PRIx64 "\n",
|
||||
reinterpret_cast<uint64_t>(dst_gpu_addr), value);
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipeClockCounterWithWriteBack(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint64_t* dst_gpu_addr, uint64_t value) {
|
||||
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), 0,
|
||||
EndOfPipeWriteSize::Qword, EndOfPipeWriteAction::WriteBack);
|
||||
|
||||
LOGF_COLOR(Log::Color::BrightGreen,
|
||||
"EndOfPipe Signal!!! [0x%016" PRIx64 "] <- Clock: 0x%016" PRIx64 "\n",
|
||||
reinterpret_cast<uint64_t>(dst_gpu_addr), value);
|
||||
}
|
||||
|
||||
void WriteAtEndOfPipeWithWriteBack64(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint64_t* dst_gpu_addr, uint64_t value) {
|
||||
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
|
||||
|
||||
@@ -25,10 +25,6 @@ void WriteAtEndOfPipe64(uint64_t submit_id, CommandBuffer& buffer, uint64_t* dst
|
||||
uint64_t value);
|
||||
void WriteAtEndOfPipeGds32(uint64_t submit_id, CommandBuffer& buffer, uint32_t* dst_gpu_addr,
|
||||
uint32_t dw_offset, uint32_t dw_num);
|
||||
void WriteAtEndOfPipeClockCounter(uint64_t submit_id, CommandBuffer& buffer, uint64_t* dst_gpu_addr,
|
||||
uint64_t value);
|
||||
void WriteAtEndOfPipeClockCounterWithWriteBack(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint64_t* dst_gpu_addr, uint64_t value);
|
||||
void WriteAtEndOfPipeWithWriteBack32(uint64_t submit_id, CommandBuffer& buffer,
|
||||
uint32_t* dst_gpu_addr, uint32_t value);
|
||||
void WriteAtEndOfPipeWithWriteBack64(uint64_t submit_id, CommandBuffer& buffer,
|
||||
|
||||
@@ -1719,7 +1719,7 @@ static AvPlayerInternal* create_player(const AvPlayerMemAllocator& mem,
|
||||
h->mem = mem;
|
||||
h->file = file;
|
||||
h->event = event;
|
||||
h->auto_start = auto_start;
|
||||
h->auto_start = auto_start || event.event_callback == nullptr;
|
||||
h->video_buffers = std::clamp(video_buffers <= 0 ? 2 : video_buffers, 2, 16);
|
||||
h->post_init.demux_video_buffer_size = 4 * 1024 * 1024;
|
||||
return h;
|
||||
|
||||
+287
-96
@@ -16,6 +16,7 @@
|
||||
#include <limits>
|
||||
#include <magic_enum.hpp>
|
||||
#include <memory>
|
||||
#include <numbers>
|
||||
#include <vector>
|
||||
|
||||
#include "libatrac9.h"
|
||||
@@ -385,8 +386,114 @@ struct Ngs2RackInternal {
|
||||
std::array<Ngs2UserFxOption, 24> fx {};
|
||||
};
|
||||
|
||||
enum class Ngs2VoicePlayState { Empty, Playing, Paused, Stopped };
|
||||
enum class Ngs2VoicePlayEvent { None, Play, Pause, Resume, Stop, StopImm, Kill };
|
||||
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;
|
||||
float frequency;
|
||||
float q;
|
||||
float level;
|
||||
uint32_t reserved[3];
|
||||
};
|
||||
static_assert(sizeof(Ngs2SamplerFilterParam) == 48);
|
||||
|
||||
struct Ngs2SamplerFilter {
|
||||
struct History {
|
||||
double x1 = 0, x2 = 0, y1 = 0, y2 = 0;
|
||||
};
|
||||
bool enabled = false;
|
||||
bool warned = false;
|
||||
uint64_t channel_mask = 0;
|
||||
double b0 = 1, b1 = 0, b2 = 0, a1 = 0, a2 = 0;
|
||||
std::vector<History> history;
|
||||
|
||||
void Configure(const Ngs2SamplerFilterParam& param, uint32_t rate, uint32_t channels) {
|
||||
if (param.type == 0) {
|
||||
enabled = false;
|
||||
history.clear();
|
||||
return;
|
||||
}
|
||||
if (param.location != 1 || param.type != 1 || !std::isfinite(param.frequency) ||
|
||||
param.frequency < 0 || !std::isfinite(param.q) || param.q <= 0 ||
|
||||
!std::isfinite(param.level) || param.level < 0 || rate == 0) {
|
||||
enabled = false;
|
||||
history.clear();
|
||||
if (!warned) {
|
||||
Log::WriteToConsoleAndLog(fmt::sprintf(
|
||||
"warning: unsupported NGS2 sampler filter (location=%u, type=%u)\n",
|
||||
param.location, param.type));
|
||||
warned = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
channel_mask = param.channel_mask;
|
||||
history.resize(channels);
|
||||
enabled = true;
|
||||
if (param.frequency == 0 || param.frequency >= rate * 0.5) {
|
||||
b0 = param.frequency == 0 ? 0 : param.level;
|
||||
b1 = b2 = a1 = a2 = 0;
|
||||
return;
|
||||
}
|
||||
// RBJ low-pass biquad, normalized by a0 (Audio EQ Cookbook).
|
||||
const double omega = (2.0 * std::numbers::pi) * param.frequency / rate;
|
||||
const double cosine = std::cos(omega);
|
||||
const double alpha = std::sin(omega) / (2.0 * param.q);
|
||||
const double inverse_a0 = 1.0 / (1.0 + alpha);
|
||||
b0 = (1.0 - cosine) * 0.5 * inverse_a0 * param.level;
|
||||
b1 = 2.0 * b0;
|
||||
b2 = b0;
|
||||
a1 = -2.0 * cosine * inverse_a0;
|
||||
a2 = (1.0 - alpha) * inverse_a0;
|
||||
}
|
||||
|
||||
bool Bypasses(size_t channel) const {
|
||||
return channel < 64 && (channel_mask & (uint64_t {1} << channel)) != 0;
|
||||
}
|
||||
|
||||
bool HasHistory() const {
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
for (size_t c = 0; c < history.size(); ++c) {
|
||||
if (Bypasses(c)) {
|
||||
continue;
|
||||
}
|
||||
const auto& h = history[c];
|
||||
if (h.x1 != 0 || h.x2 != 0 || h.y1 != 0 || h.y2 != 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Process(std::vector<float>& samples, uint32_t channels, uint32_t grain) {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
for (uint32_t c = 0; c < channels; ++c) {
|
||||
if (Bypasses(c)) {
|
||||
continue;
|
||||
}
|
||||
auto& h = history[c];
|
||||
for (uint32_t i = 0; i < grain; ++i) {
|
||||
const double x = samples[c * grain + i];
|
||||
double y = b0 * x + b1 * h.x1 + b2 * h.x2 - a1 * h.y1 - a2 * h.y2;
|
||||
if (std::abs(y) < 1e-20) {
|
||||
y = 0;
|
||||
}
|
||||
h.x2 = h.x1;
|
||||
h.x1 = x;
|
||||
h.y2 = h.y1;
|
||||
h.y1 = y;
|
||||
samples[c * grain + i] = static_cast<float>(y);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct Ngs2VoiceInternal {
|
||||
struct Port {
|
||||
@@ -401,6 +508,7 @@ struct Ngs2VoiceInternal {
|
||||
uint32_t cursor = 0;
|
||||
size_t data_cursor = 0;
|
||||
uint32_t skip_remaining = info.num_skip_samples;
|
||||
uint32_t repeated_count = 0;
|
||||
};
|
||||
struct Module {
|
||||
std::vector<uint8_t> param, work, state;
|
||||
@@ -408,8 +516,8 @@ struct Ngs2VoiceInternal {
|
||||
bool control_warned = false;
|
||||
bool render_warned = false;
|
||||
};
|
||||
Ngs2VoicePlayEvent event = Ngs2VoicePlayEvent::None;
|
||||
Ngs2VoicePlayState state = Ngs2VoicePlayState::Empty;
|
||||
uint32_t state_flags = 0;
|
||||
Ngs2RackInternal* rack = nullptr;
|
||||
uintptr_t callback = 0;
|
||||
uintptr_t callback_data = 0;
|
||||
@@ -428,21 +536,45 @@ struct Ngs2VoiceInternal {
|
||||
|
||||
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;
|
||||
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<Ngs2SamplerFilter> filters;
|
||||
|
||||
const uint8_t* WaveformData() const {
|
||||
if (blocks.empty()) {
|
||||
return waveform_end;
|
||||
}
|
||||
const auto& block = blocks.front();
|
||||
return block.data + (decoder != nullptr
|
||||
? block.data_cursor
|
||||
: (uint64_t(block.info.num_skip_samples) + block.cursor) *
|
||||
channels * sizeof(int16_t));
|
||||
}
|
||||
|
||||
void SetupSampler(const Ngs2WaveformFormat& format) {
|
||||
EXIT_NOT_IMPLEMENTED(format.frame_margin != 0 || format.frame_offset != 0 ||
|
||||
format.sample_rate == 0 ||
|
||||
format.sample_rate == 0 || format.num_channels == 0 ||
|
||||
(format.waveform_type == NGS2_WAVEFORM_TYPE_ATRAC9 &&
|
||||
format.sample_rate != rack->ngs->option.sample_rate));
|
||||
channels = format.num_channels;
|
||||
sample_rate = format.sample_rate;
|
||||
sample_phase = 0;
|
||||
sample_step = uint64_t(sample_rate) << 32u;
|
||||
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();
|
||||
decoder.reset();
|
||||
decoded_frame.clear();
|
||||
accepts_blocks = true;
|
||||
@@ -481,12 +613,30 @@ struct Ngs2VoiceInternal {
|
||||
}
|
||||
void SetEvent(uint32_t id) {
|
||||
switch (id) {
|
||||
case 1: event = Ngs2VoicePlayEvent::Play; break;
|
||||
case 2: event = Ngs2VoicePlayEvent::Stop; break;
|
||||
case 4: event = Ngs2VoicePlayEvent::StopImm; break;
|
||||
case 8: event = Ngs2VoicePlayEvent::Kill; break;
|
||||
case 16: event = Ngs2VoicePlayEvent::Pause; break;
|
||||
case 32: event = Ngs2VoicePlayEvent::Resume; break;
|
||||
case 1:
|
||||
if (state == Ngs2VoicePlayState::Empty || state == Ngs2VoicePlayState::Stopped) {
|
||||
state = Ngs2VoicePlayState::Playing;
|
||||
// Reserve the voice immediately; publish playback flags after rendering.
|
||||
state_flags |= 1;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
if (state == Ngs2VoicePlayState::Playing || state == Ngs2VoicePlayState::Paused) {
|
||||
state = Ngs2VoicePlayState::Stopped;
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
case 8: state = Ngs2VoicePlayState::Empty; break;
|
||||
case 16:
|
||||
if (state == Ngs2VoicePlayState::Playing) {
|
||||
state = Ngs2VoicePlayState::Paused;
|
||||
}
|
||||
break;
|
||||
case 32:
|
||||
if (state == Ngs2VoicePlayState::Paused) {
|
||||
state = Ngs2VoicePlayState::Playing;
|
||||
}
|
||||
break;
|
||||
default: EXIT("unknown event_id: 0x%08" PRIx32 "\n", id);
|
||||
}
|
||||
}
|
||||
@@ -603,14 +753,7 @@ static_assert(sizeof(Ngs2WaveformBlock) == 40);
|
||||
static_assert(sizeof(Ngs2WaveformInfo) == 232);
|
||||
|
||||
static uint32_t Ngs2GetStateFlags(const Ngs2VoiceInternal* voice) {
|
||||
switch (voice->state) {
|
||||
case Ngs2VoicePlayState::Empty: return 0;
|
||||
case Ngs2VoicePlayState::Playing: return 0x3;
|
||||
case Ngs2VoicePlayState::Paused: return 0x5;
|
||||
case Ngs2VoicePlayState::Stopped: return 0xb;
|
||||
}
|
||||
|
||||
return 0;
|
||||
return voice->state_flags;
|
||||
}
|
||||
|
||||
static Ngs2SystemOption Ngs2DefaultSystemOption() {
|
||||
@@ -1235,92 +1378,83 @@ int KYTY_SYSV_ABI Ngs2RackUnlock(uintptr_t rack_handle) {
|
||||
return OK;
|
||||
}
|
||||
|
||||
static void Ngs2ApplyEvent(Ngs2VoiceInternal& voice) {
|
||||
switch (voice.event) {
|
||||
case Ngs2VoicePlayEvent::None: break;
|
||||
case Ngs2VoicePlayEvent::Play:
|
||||
if (voice.state == Ngs2VoicePlayState::Empty) {
|
||||
voice.state = Ngs2VoicePlayState::Playing;
|
||||
}
|
||||
break;
|
||||
case Ngs2VoicePlayEvent::Pause:
|
||||
if (voice.state == Ngs2VoicePlayState::Playing) {
|
||||
voice.state = Ngs2VoicePlayState::Paused;
|
||||
}
|
||||
break;
|
||||
case Ngs2VoicePlayEvent::Resume:
|
||||
if (voice.state == Ngs2VoicePlayState::Paused) {
|
||||
voice.state = Ngs2VoicePlayState::Playing;
|
||||
}
|
||||
break;
|
||||
case Ngs2VoicePlayEvent::Stop:
|
||||
if (voice.state == Ngs2VoicePlayState::Playing) {
|
||||
voice.state = Ngs2VoicePlayState::Stopped;
|
||||
}
|
||||
break;
|
||||
case Ngs2VoicePlayEvent::StopImm:
|
||||
case Ngs2VoicePlayEvent::Kill: voice.state = Ngs2VoicePlayState::Empty; break;
|
||||
}
|
||||
voice.event = Ngs2VoicePlayEvent::None;
|
||||
}
|
||||
struct Ngs2VoiceCallbackInfo {
|
||||
uintptr_t data, voice;
|
||||
uint32_t flag, reserved;
|
||||
uintptr_t user;
|
||||
const void* block_data;
|
||||
size_t size;
|
||||
uint32_t repeats, attributes;
|
||||
};
|
||||
static_assert(sizeof(Ngs2VoiceCallbackInfo) == 56);
|
||||
|
||||
static void Ngs2FinishBlock(Ngs2VoiceInternal& voice) {
|
||||
const auto& block = voice.blocks.front();
|
||||
struct CallbackInfo {
|
||||
uintptr_t data, voice;
|
||||
uint32_t flag, reserved;
|
||||
uintptr_t user;
|
||||
const void* block_data;
|
||||
size_t size;
|
||||
uint32_t repeats, attributes;
|
||||
} info {voice.callback_data,
|
||||
reinterpret_cast<uintptr_t>(&voice),
|
||||
1,
|
||||
0,
|
||||
block.info.user_data,
|
||||
block.data,
|
||||
block.info.data_size,
|
||||
0,
|
||||
0};
|
||||
static_assert(sizeof(CallbackInfo) == 56);
|
||||
voice.blocks.pop_front();
|
||||
if (voice.blocks.empty() && !voice.accepts_blocks) {
|
||||
voice.state = Ngs2VoicePlayState::Empty;
|
||||
auto& block = voice.blocks.front();
|
||||
const bool repeat = block.info.num_repeats != 0;
|
||||
voice.waveform_end = voice.WaveformData();
|
||||
if (repeat) {
|
||||
if (block.info.num_repeats != UINT32_MAX) {
|
||||
--block.info.num_repeats;
|
||||
}
|
||||
++block.repeated_count;
|
||||
block.cursor = 0;
|
||||
}
|
||||
if (voice.callback != 0 && (voice.callback_flags & 1u) != 0) {
|
||||
reinterpret_cast<void KYTY_SYSV_ABI (*)(const CallbackInfo*)>(voice.callback)(&info);
|
||||
const Ngs2VoiceCallbackInfo info {voice.callback_data,
|
||||
reinterpret_cast<uintptr_t>(&voice),
|
||||
repeat ? 2u : 1u,
|
||||
0,
|
||||
block.info.user_data,
|
||||
block.data,
|
||||
block.info.data_size,
|
||||
block.repeated_count,
|
||||
0};
|
||||
if (!repeat) {
|
||||
voice.blocks.pop_front();
|
||||
}
|
||||
if (voice.callback != 0 && (voice.callback_flags & info.flag) != 0) {
|
||||
reinterpret_cast<void KYTY_SYSV_ABI (*)(const Ngs2VoiceCallbackInfo*)>(voice.callback)(
|
||||
&info);
|
||||
}
|
||||
}
|
||||
|
||||
static void Ngs2AdvancePcm(Ngs2VoiceInternal& voice) {
|
||||
const auto output_rate = uint64_t(voice.rack->ngs->option.sample_rate) << 32u;
|
||||
while (!voice.blocks.empty() && voice.sample_phase >= output_rate) {
|
||||
// Completion callbacks may interrupt advancement across multiple blocks.
|
||||
while (voice.state == Ngs2VoicePlayState::Playing && !voice.blocks.empty() &&
|
||||
voice.sample_phase >= output_rate) {
|
||||
auto& block = voice.blocks.front();
|
||||
const auto available = block.info.num_samples - block.cursor;
|
||||
const auto advance = std::min(voice.sample_phase / output_rate,
|
||||
uint64_t(available));
|
||||
block.cursor += static_cast<uint32_t>(advance);
|
||||
voice.decoded_samples += advance;
|
||||
voice.decoded_bytes += advance * voice.channels * sizeof(int16_t);
|
||||
voice.sample_phase -= advance * output_rate;
|
||||
if (block.cursor == block.info.num_samples) {
|
||||
Ngs2FinishBlock(voice);
|
||||
}
|
||||
}
|
||||
if (voice.blocks.empty() && !voice.accepts_blocks) {
|
||||
voice.sample_phase = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) {
|
||||
uint32_t output = 0;
|
||||
while (output < grain && !voice.blocks.empty()) {
|
||||
while (voice.state == Ngs2VoicePlayState::Playing && output < grain && !voice.blocks.empty()) {
|
||||
if (voice.decoder == nullptr) {
|
||||
Ngs2AdvancePcm(voice);
|
||||
if (voice.blocks.empty()) {
|
||||
if (voice.state != Ngs2VoicePlayState::Playing || voice.blocks.empty()) {
|
||||
break;
|
||||
}
|
||||
const auto& block = voice.blocks.front();
|
||||
const auto* pcm = reinterpret_cast<const int16_t*>(block.data) +
|
||||
(block.info.num_skip_samples + block.cursor) * voice.channels;
|
||||
const auto* pcm = reinterpret_cast<const int16_t*>(voice.WaveformData());
|
||||
const auto* next = pcm;
|
||||
if (block.cursor + 1 < block.info.num_samples) {
|
||||
next += voice.channels;
|
||||
} else if (block.info.num_repeats != 0) {
|
||||
next = reinterpret_cast<const int16_t*>(block.data) +
|
||||
block.info.num_skip_samples * voice.channels;
|
||||
} else if (voice.blocks.size() > 1) {
|
||||
const auto& next_block = voice.blocks[1];
|
||||
next = reinterpret_cast<const int16_t*>(next_block.data) +
|
||||
@@ -1331,8 +1465,7 @@ static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) {
|
||||
<< 32u);
|
||||
for (uint32_t c = 0; c < voice.channels; ++c) {
|
||||
voice.samples[c * grain + output] =
|
||||
(static_cast<float>(pcm[c]) +
|
||||
(static_cast<float>(next[c]) - pcm[c]) * fraction) /
|
||||
std::lerp(static_cast<float>(pcm[c]), static_cast<float>(next[c]), fraction) /
|
||||
32768.0f;
|
||||
}
|
||||
voice.sample_phase += voice.sample_step;
|
||||
@@ -1369,7 +1502,9 @@ static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) {
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1389,8 +1524,22 @@ static void Ngs2RenderVoice(Ngs2VoiceInternal& voice, const std::vector<Ngs2Voic
|
||||
voice.samples.assign(grain * voice.channels, 0.0f);
|
||||
voice.has_samples = false;
|
||||
if (voice.state == Ngs2VoicePlayState::Playing) {
|
||||
if (voice.rack->type == Ngs2RackType::CustomSampler) {
|
||||
if (voice.rack->type == Ngs2RackType::Sampler ||
|
||||
voice.rack->type == Ngs2RackType::CustomSampler) {
|
||||
Ngs2ConsumeSamples(voice, grain);
|
||||
// A playing stream can have no source data but still have a filter tail.
|
||||
for (auto& filter: voice.filters) {
|
||||
if (voice.has_samples || filter.HasHistory()) {
|
||||
filter.Process(voice.samples, voice.channels, grain);
|
||||
voice.has_samples = true;
|
||||
}
|
||||
}
|
||||
if (voice.state == Ngs2VoicePlayState::Playing && voice.blocks.empty() &&
|
||||
!voice.accepts_blocks &&
|
||||
std::ranges::none_of(voice.filters,
|
||||
[](const auto& filter) { return filter.HasHistory(); })) {
|
||||
voice.state = Ngs2VoicePlayState::Empty;
|
||||
}
|
||||
}
|
||||
for (auto* source: voices) {
|
||||
for (const auto& port: source->ports) {
|
||||
@@ -1472,11 +1621,8 @@ int KYTY_SYSV_ABI Ngs2SystemRender(uintptr_t system_handle, const Ngs2RenderBuff
|
||||
}
|
||||
auto* items = reinterpret_cast<Ngs2VoiceInternal*>(rack + 1);
|
||||
for (uint32_t i = 0; i < rack->option.common.max_voices; ++i) {
|
||||
Ngs2ApplyEvent(items[i]);
|
||||
items[i].rendered = false;
|
||||
if (items[i].channels != 0) {
|
||||
voices.push_back(items + i);
|
||||
}
|
||||
voices.push_back(items + i);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1487,6 +1633,9 @@ 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;
|
||||
@@ -1509,6 +1658,13 @@ int KYTY_SYSV_ABI Ngs2SystemRender(uintptr_t system_handle, const Ngs2RenderBuff
|
||||
}
|
||||
}
|
||||
}
|
||||
for (auto* voice: voices) {
|
||||
// No envelope is configured, so a release stop finishes in this render.
|
||||
if (voice->state == Ngs2VoicePlayState::Stopped) {
|
||||
voice->state = Ngs2VoicePlayState::Empty;
|
||||
}
|
||||
voice->state_flags = static_cast<uint32_t>(voice->state);
|
||||
}
|
||||
++ngs->render_count;
|
||||
return OK;
|
||||
}
|
||||
@@ -1920,7 +2076,6 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0x1000: EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Sampler); break;
|
||||
case 0x2000: EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Submixer); break;
|
||||
case 0x2001: EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Reverb); break;
|
||||
case 0x3000: {
|
||||
@@ -1969,8 +2124,11 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
|
||||
module.flags |= 2;
|
||||
break;
|
||||
}
|
||||
case 0x1000:
|
||||
case 0x4001: {
|
||||
EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::CustomSampler);
|
||||
EXIT_NOT_IMPLEMENTED(
|
||||
voice->rack->type !=
|
||||
(rack_id == 0x1000 ? Ngs2RackType::Sampler : Ngs2RackType::CustomSampler));
|
||||
switch (param->id & 0xffffu) {
|
||||
case 0: {
|
||||
const auto& format =
|
||||
@@ -1987,16 +2145,21 @@ 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 != 0x11 &&
|
||||
(blocks.flags != 0 && blocks.flags != 1 &&
|
||||
blocks.flags != 0x11 &&
|
||||
blocks.flags != 0x4));
|
||||
if (blocks.flags == 0x4) {
|
||||
voice->blocks.clear();
|
||||
voice->sample_phase = 0;
|
||||
voice->waveform_end = nullptr;
|
||||
}
|
||||
voice->accepts_blocks = (blocks.flags & 1u) != 0;
|
||||
for (uint32_t i = 0; i < blocks.count; ++i) {
|
||||
const auto& block = blocks.blocks[i];
|
||||
EXIT_NOT_IMPLEMENTED(block.num_repeats != 0 || block.num_samples == 0);
|
||||
EXIT_NOT_IMPLEMENTED(block.num_samples == 0);
|
||||
EXIT_NOT_IMPLEMENTED(block.num_repeats != 0 &&
|
||||
(voice->rack->type != Ngs2RackType::Sampler ||
|
||||
voice->decoder != nullptr));
|
||||
EXIT_NOT_IMPLEMENTED(
|
||||
voice->decoder == nullptr &&
|
||||
(uint64_t(block.num_skip_samples) + block.num_samples) *
|
||||
@@ -2006,6 +2169,16 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 4: {
|
||||
EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Sampler);
|
||||
for (auto& block: voice->blocks) {
|
||||
if (block.info.num_repeats != 0) {
|
||||
block.info.num_repeats = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 5: {
|
||||
const float ratio = *reinterpret_cast<const float*>(param + 1);
|
||||
EXIT_NOT_IMPLEMENTED(!std::isfinite(ratio) || ratio < 0.0f || ratio > 4.0f ||
|
||||
@@ -2014,6 +2187,21 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
|
||||
static_cast<double>(voice->sample_rate) * ratio * 4294967296.0));
|
||||
break;
|
||||
}
|
||||
case 0xa: {
|
||||
EXIT_NOT_IMPLEMENTED(voice->rack->type != Ngs2RackType::Sampler);
|
||||
EXIT_NOT_IMPLEMENTED(param->size != sizeof(Ngs2VoiceParamHeader) +
|
||||
sizeof(Ngs2SamplerFilterParam));
|
||||
const auto& filter =
|
||||
*reinterpret_cast<const Ngs2SamplerFilterParam*>(param + 1);
|
||||
EXIT_NOT_IMPLEMENTED(filter.index >=
|
||||
voice->rack->option.sampler.max_filters);
|
||||
if (voice->filters.size() <= filter.index) {
|
||||
voice->filters.resize(filter.index + 1);
|
||||
}
|
||||
voice->filters[filter.index].Configure(
|
||||
filter, voice->rack->ngs->option.sample_rate, voice->channels);
|
||||
break;
|
||||
}
|
||||
default: EXIT("unsupported sampler control: 0x%08" PRIx32 "\n", param->id);
|
||||
}
|
||||
break;
|
||||
@@ -2141,9 +2329,12 @@ int KYTY_SYSV_ABI Ngs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* stat
|
||||
sampler->envelope_height = 1.0f;
|
||||
sampler->peak_height = 0.0f;
|
||||
sampler->reserved = 0;
|
||||
sampler->num_decoded_samples = 0;
|
||||
sampler->user_data = 0;
|
||||
sampler->waveform_data = nullptr;
|
||||
sampler->num_decoded_samples = voice->decoded_samples;
|
||||
sampler->decoded_data_size = voice->decoded_bytes;
|
||||
sampler->waveform_data = voice->WaveformData();
|
||||
if (!voice->blocks.empty()) {
|
||||
sampler->user_data = voice->blocks.front().info.user_data;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: EXIT("unknown type: %s\n", magic_enum::enum_name(voice->rack->type));
|
||||
|
||||
@@ -0,0 +1,689 @@
|
||||
// Exercise sampler controls and rendering without a host audio device.
|
||||
#include "libs/ngs2.cpp"
|
||||
|
||||
#include <bit>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <numbers>
|
||||
|
||||
using namespace Libs::Audio::Ngs2;
|
||||
|
||||
namespace {
|
||||
|
||||
void Check(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::fprintf(stderr, "Ngs2SamplerTests: %s\n", message);
|
||||
std::abort();
|
||||
}
|
||||
}
|
||||
|
||||
struct Fixture {
|
||||
Ngs2Internal system;
|
||||
Ngs2RackInternal rack {};
|
||||
Ngs2VoiceInternal voice;
|
||||
|
||||
Fixture(uint32_t rate, uint32_t channels = 1, Ngs2RackType type = Ngs2RackType::Sampler) {
|
||||
system.option.sample_rate = 48000;
|
||||
rack.ngs = &system;
|
||||
rack.type = type;
|
||||
voice.rack = &rack;
|
||||
struct Setup {
|
||||
Ngs2VoiceParamHeader header {40, 0, 0x10000000};
|
||||
Ngs2WaveformFormat format;
|
||||
uint32_t flags = 0, reserved = 0;
|
||||
} setup;
|
||||
setup.header.id = type == Ngs2RackType::Sampler ? 0x10000000 : 0x40010000;
|
||||
setup.format = {0x12, channels, rate};
|
||||
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&voice), &setup.header) == OK,
|
||||
"sampler setup failed");
|
||||
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};
|
||||
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.header.id = rack.type == Ngs2RackType::Sampler ? 0x10000001 : 0x40010001;
|
||||
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&voice), ¶m.header) == OK,
|
||||
"sampler queue failed");
|
||||
}
|
||||
|
||||
void Render(uint32_t frames) {
|
||||
voice.rendered = false;
|
||||
Ngs2RenderVoice(voice, {}, frames);
|
||||
}
|
||||
};
|
||||
|
||||
void TestStreamingResample() {
|
||||
Fixture f(44100, 2);
|
||||
std::vector<int16_t> pcm(441 * 2);
|
||||
for (int i = 0; i < 441; ++i) {
|
||||
pcm[i * 2] = static_cast<int16_t>(i * 32);
|
||||
pcm[i * 2 + 1] = static_cast<int16_t>(-i * 32);
|
||||
}
|
||||
f.Queue(pcm.data(), 200, 1);
|
||||
f.Queue(pcm.data() + 400, 241, 0);
|
||||
for (uint32_t grain = 0; grain < 3; ++grain) {
|
||||
f.Render(160);
|
||||
for (uint32_t i = 0; i < 160; ++i) {
|
||||
const float expected = std::min((grain * 160 + i) * 44100.0 / 48000.0, 440.0) / 1024.0;
|
||||
Check(std::abs(f.voice.samples[i] - expected) < 0.00001f,
|
||||
"resampling discontinuity across grain or block boundary");
|
||||
Check(std::abs(f.voice.samples[160 + i] + expected) < 0.00001f,
|
||||
"stereo channels were mixed");
|
||||
}
|
||||
}
|
||||
Check(f.voice.blocks.empty(), "final streaming block did not finish");
|
||||
Check(f.voice.state == Ngs2VoicePlayState::Empty, "finished voice remained active");
|
||||
Ngs2SamplerVoiceState state {};
|
||||
Ngs2VoiceGetState(reinterpret_cast<uintptr_t>(&f.voice), &state.voice_state, sizeof(state));
|
||||
Check(state.num_decoded_samples == 441 && state.decoded_data_size == pcm.size() * 2,
|
||||
"streaming progress does not match consumed source data");
|
||||
}
|
||||
|
||||
void TestPitchLoopAndSkip() {
|
||||
Fixture f(24000);
|
||||
const int16_t pcm[] = {-30000, 8192, 16384};
|
||||
f.Queue(pcm, 2, 0, 1, 1);
|
||||
struct Pitch {
|
||||
Ngs2VoiceParamHeader header {16, 0, 0x10000005};
|
||||
float ratio = 2.0f;
|
||||
uint32_t reserved = 0;
|
||||
} pitch;
|
||||
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&f.voice), &pitch.header);
|
||||
f.Render(5);
|
||||
const float expected[] = {0.25f, 0.5f, 0.25f, 0.5f, 0.0f};
|
||||
for (size_t i = 0; i < 5; ++i) {
|
||||
Check(std::abs(f.voice.samples[i] - expected[i]) < 0.00001f,
|
||||
"pitch, repeat count, or skipped samples incorrect");
|
||||
}
|
||||
}
|
||||
|
||||
void TestStarvationAndPause() {
|
||||
Fixture f(48000);
|
||||
const int16_t pcm[] = {16384, 8192};
|
||||
f.Queue(pcm, 2, 1);
|
||||
f.Render(4);
|
||||
Check(f.voice.state == Ngs2VoicePlayState::Playing, "stream starvation stopped voice");
|
||||
Check(f.voice.samples[2] == 0.0f, "starved output was not silent");
|
||||
f.Queue(pcm, 2, 0);
|
||||
f.voice.SetEvent(16);
|
||||
f.Render(2);
|
||||
Check(!f.voice.has_samples && f.voice.blocks.front().cursor == 0, "paused voice consumed data");
|
||||
f.voice.SetEvent(32);
|
||||
f.Render(2);
|
||||
Check(f.voice.samples[0] == 0.5f && f.voice.samples[1] == 0.25f,
|
||||
"stream failed to resume after starvation/pause");
|
||||
}
|
||||
|
||||
void TestMonoRateAndRouting() {
|
||||
Fixture f(22050);
|
||||
std::vector<int16_t> pcm(441, 16384);
|
||||
f.Queue(pcm.data(), 441, 0);
|
||||
Ngs2RackInternal mastering_rack {};
|
||||
mastering_rack.type = Ngs2RackType::Mastering;
|
||||
Ngs2VoiceInternal mastering;
|
||||
mastering.rack = &mastering_rack;
|
||||
mastering.channels = 2;
|
||||
mastering.state = Ngs2VoicePlayState::Playing;
|
||||
f.voice.matrices = {{0.5f, 0.25f}};
|
||||
f.voice.ports.push_back({&mastering, 0, 1.0f, 0});
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
mastering.rendered = false;
|
||||
f.voice.rendered = false;
|
||||
Ngs2RenderVoice(mastering, {&f.voice, &mastering}, 320);
|
||||
Check(mastering.has_samples, "standard sampler did not reach mastering voice");
|
||||
for (uint32_t j = 0; j < 320; ++j) {
|
||||
Check(mastering.samples[j] == 0.25f && mastering.samples[320 + j] == 0.125f,
|
||||
"mono PCM routing matrix was not applied");
|
||||
}
|
||||
}
|
||||
Check(f.voice.decoded_samples == 441 && f.voice.blocks.empty(),
|
||||
"22.05 kHz effect duration was incorrect");
|
||||
}
|
||||
|
||||
void TestCustomPcmStillPlays() {
|
||||
Fixture f(48000, 1, Ngs2RackType::CustomSampler);
|
||||
const int16_t pcm[] = {8192, -16384};
|
||||
f.Queue(pcm, 2, 0);
|
||||
f.Render(4);
|
||||
Check(f.voice.samples[0] == 0.25f && f.voice.samples[1] == -0.5f &&
|
||||
f.voice.samples[2] == 0.0f && f.voice.blocks.empty(),
|
||||
"custom PCM sampler regressed");
|
||||
}
|
||||
|
||||
void TestSamplerReuseResetsRouting() {
|
||||
for (auto type: {Ngs2RackType::Sampler, Ngs2RackType::CustomSampler}) {
|
||||
Fixture f(48000, 1, type);
|
||||
Ngs2RackInternal mastering_rack {};
|
||||
mastering_rack.type = Ngs2RackType::Mastering;
|
||||
Ngs2VoiceInternal mastering;
|
||||
mastering.rack = &mastering_rack;
|
||||
mastering.channels = 2;
|
||||
mastering.state = Ngs2VoicePlayState::Playing;
|
||||
f.voice.ports.push_back({&mastering, 1, 0.25f, 0});
|
||||
f.voice.matrices = {{0.5f, 0.25f}};
|
||||
f.voice.SetupSampler({0x12, 2, 48000});
|
||||
Check(f.voice.ports.size() == 1 && f.voice.ports[0].dest == nullptr &&
|
||||
f.voice.ports[0].input == 0 && f.voice.ports[0].volume == 1.0f &&
|
||||
f.voice.ports[0].matrix == -1 && f.voice.matrices.size() == 1 &&
|
||||
f.voice.matrices[0].empty(),
|
||||
"sampler setup retained routing from the previous channel layout");
|
||||
const int16_t pcm[] = {8192, 16384, -8192, -16384};
|
||||
f.Queue(pcm, 2, 0);
|
||||
f.Render(1);
|
||||
Check(!f.voice.has_samples && f.voice.blocks.front().cursor == 0,
|
||||
"sampler setup started the new waveform without a play event");
|
||||
f.voice.ports[0].dest = &mastering;
|
||||
f.voice.SetEvent(1);
|
||||
f.voice.rendered = false;
|
||||
Ngs2RenderVoice(mastering, {&f.voice, &mastering}, 2);
|
||||
Check(mastering.samples == std::vector<float> {0.25f, -0.25f, 0.5f, -0.5f},
|
||||
"reused stereo sampler did not restore default channel routing");
|
||||
}
|
||||
}
|
||||
|
||||
void TestPlayStateBeforeRender() {
|
||||
Fixture f(48000);
|
||||
const int16_t pcm[] = {8192, -16384};
|
||||
f.voice.state = Ngs2VoicePlayState::Empty;
|
||||
f.Queue(pcm, 2, 0);
|
||||
Ngs2VoiceEventParam event {{sizeof(Ngs2VoiceEventParam), 0, 6}, 1};
|
||||
const auto handle = reinterpret_cast<uintptr_t>(&f.voice);
|
||||
Ngs2VoiceControl(handle, &event.header);
|
||||
uint32_t flags = 0;
|
||||
Ngs2VoiceGetStateFlags(handle, &flags);
|
||||
Check(flags == 1, "play must reserve the voice before rendering starts playback");
|
||||
Ngs2SamplerVoiceState state {};
|
||||
Ngs2VoiceGetState(handle, &state.voice_state, sizeof(state));
|
||||
Check(state.voice_state.state_flags == flags && state.num_decoded_samples == 0,
|
||||
"state queries disagreed or play consumed samples before render");
|
||||
f.Render(4);
|
||||
Check(f.voice.samples[0] == 0.25f && f.voice.samples[1] == -0.5f,
|
||||
"one-shot effect was dropped before its first render");
|
||||
}
|
||||
|
||||
void TestStatePublication() {
|
||||
Ngs2Internal system;
|
||||
system.option = Ngs2DefaultSystemOption();
|
||||
Ngs2RackOptionUnion option {};
|
||||
Ngs2FillDefaultRackOption(0x1000, &option);
|
||||
option.common.max_voices = 1;
|
||||
alignas(Ngs2RackInternal) alignas(Ngs2VoiceInternal)
|
||||
std::byte storage[sizeof(Ngs2RackInternal) + sizeof(Ngs2VoiceInternal)];
|
||||
Ngs2ContextBufferInfo buffer {storage, sizeof(storage)};
|
||||
uintptr_t rack_handle = 0;
|
||||
const auto system_handle = reinterpret_cast<uintptr_t>(&system);
|
||||
Check(Ngs2RackCreate(system_handle, 0x1000, &option.common, &buffer, &rack_handle) == OK,
|
||||
"state publication rack creation failed");
|
||||
auto& voice =
|
||||
*reinterpret_cast<Ngs2VoiceInternal*>(reinterpret_cast<Ngs2RackInternal*>(rack_handle) + 1);
|
||||
voice.SetupSampler({0x12, 1, 48000});
|
||||
const auto handle = reinterpret_cast<uintptr_t>(&voice);
|
||||
auto check_flags = [&](uint32_t expected) {
|
||||
uint32_t flags = 0;
|
||||
Ngs2SamplerVoiceState state {};
|
||||
Ngs2VoiceGetStateFlags(handle, &flags);
|
||||
Ngs2VoiceGetState(handle, &state.voice_state, sizeof(state));
|
||||
Check(flags == expected && state.voice_state.state_flags == expected,
|
||||
"voice state was published at the wrong render boundary");
|
||||
};
|
||||
struct Transition {
|
||||
uint32_t event, before_render, after_render;
|
||||
};
|
||||
const Transition transitions[] = {{1, 1, 3}, {16, 3, 5}, {32, 5, 3}, {2, 3, 0},
|
||||
{1, 1, 3}, {16, 3, 5}, {2, 5, 0}, {1, 1, 3},
|
||||
{4, 3, 0}, {1, 1, 3}, {8, 3, 0}};
|
||||
Ngs2RenderBufferInfo output {};
|
||||
for (const auto& transition: transitions) {
|
||||
Ngs2VoiceEventParam event {{sizeof(Ngs2VoiceEventParam), 0, 6}, transition.event};
|
||||
Ngs2VoiceControl(handle, &event.header);
|
||||
check_flags(transition.before_render);
|
||||
Ngs2SystemRender(system_handle, &output, 1);
|
||||
check_flags(transition.after_render);
|
||||
}
|
||||
Check(Ngs2RackDestroy(rack_handle, nullptr) == OK, "state publication rack cleanup failed");
|
||||
}
|
||||
|
||||
void TestStopThenPlayBeforeRender() {
|
||||
Fixture f(48000);
|
||||
const int16_t pcm[] = {16384, -8192};
|
||||
f.Queue(pcm, 2, 0);
|
||||
f.voice.SetEvent(2);
|
||||
f.voice.SetEvent(1);
|
||||
Check(Ngs2GetStateFlags(&f.voice) == 1, "stopped voice could not restart");
|
||||
f.voice.SetEvent(8);
|
||||
f.voice.SetEvent(1);
|
||||
Check(Ngs2GetStateFlags(&f.voice) == 1, "kill/play commands lost their ordering");
|
||||
f.Render(2);
|
||||
Check(f.voice.samples[0] == 0.5f && f.voice.samples[1] == -0.25f,
|
||||
"restarted voice produced no audio");
|
||||
}
|
||||
|
||||
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, {}};
|
||||
} param;
|
||||
param.filter.frequency = frequency;
|
||||
param.filter.channel_mask = mask;
|
||||
f.rack.option.sampler.max_filters = 8;
|
||||
Check(Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&f.voice), ¶m.header) == OK,
|
||||
"filter ABI command failed");
|
||||
}
|
||||
|
||||
void TestLowPassResponse() {
|
||||
// A second-order Butterworth LPF has unity DC gain and -3 dB at cutoff.
|
||||
for (double frequency: {100.0, 1000.0, 10000.0}) {
|
||||
Fixture f(48000);
|
||||
SetLowPass(f);
|
||||
std::vector<int16_t> pcm(48000);
|
||||
for (size_t i = 0; i < pcm.size(); ++i) {
|
||||
pcm[i] =
|
||||
static_cast<int16_t>(8192 * std::sin((2.0 * std::numbers::pi) * frequency * i / 48000));
|
||||
}
|
||||
f.Queue(pcm.data(), static_cast<uint32_t>(pcm.size()), 0);
|
||||
f.Render(static_cast<uint32_t>(pcm.size()));
|
||||
double input = 0, output = 0;
|
||||
for (size_t i = 24000; i < pcm.size(); ++i) {
|
||||
input += std::pow(pcm[i] / 32768.0, 2);
|
||||
output += std::pow(f.voice.samples[i], 2);
|
||||
}
|
||||
const double ratio = std::sqrt(output / input);
|
||||
if (frequency == 100) {
|
||||
Check(std::abs(ratio - 1.0) < 0.002, "low-pass lost bass");
|
||||
}
|
||||
if (frequency == 1000) {
|
||||
Check(std::abs(ratio - std::sqrt(0.5)) < 0.002, "cutoff gain incorrect");
|
||||
}
|
||||
if (frequency == 10000) {
|
||||
Check(ratio < 0.01, "low-pass did not reject high frequencies");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TestFilterHistoryAndChannels() {
|
||||
Fixture whole(48000, 2), split(48000, 2);
|
||||
SetLowPass(whole, 1000, 1);
|
||||
SetLowPass(split, 1000, 1);
|
||||
std::vector<int16_t> pcm(256 * 2, 0);
|
||||
pcm[0] = pcm[1] = 16384;
|
||||
whole.Queue(pcm.data(), 256, 0);
|
||||
split.Queue(pcm.data(), 32, 1);
|
||||
split.Queue(pcm.data() + 64, 224, 0);
|
||||
whole.Render(256);
|
||||
for (uint32_t g = 0; g < 8; ++g) {
|
||||
// Games resend unchanged parameters; this must not reset the delay line.
|
||||
SetLowPass(split, 1000, 1);
|
||||
split.Render(32);
|
||||
for (uint32_t c = 0; c < 2; ++c) {
|
||||
for (uint32_t i = 0; i < 32; ++i) {
|
||||
Check(std::abs(split.voice.samples[c * 32 + i] -
|
||||
whole.voice.samples[c * 256 + g * 32 + i]) < 1e-7,
|
||||
"filter history changed at a grain/block/control boundary");
|
||||
}
|
||||
}
|
||||
}
|
||||
Check(whole.voice.samples[0] == 0.5f && whole.voice.samples[1] == 0.0f &&
|
||||
whole.voice.samples[256] > 0.0f && whole.voice.samples[256] < 0.5f,
|
||||
"channel mask must bypass selected channels and filter the others");
|
||||
Fixture stereo(48000, 2);
|
||||
SetLowPass(stereo);
|
||||
pcm[1] = 0;
|
||||
stereo.Queue(pcm.data(), 256, 0);
|
||||
stereo.Render(256);
|
||||
for (size_t i = 256; i < 512; ++i) {
|
||||
Check(stereo.voice.samples[i] == 0.0f, "filter leaked history between channels");
|
||||
}
|
||||
stereo.voice.SetupSampler({0x12, 2, 48000});
|
||||
Check(stereo.voice.filters.empty(), "reused voice retained old filters");
|
||||
}
|
||||
|
||||
void TestFilterUsesOutputRate() {
|
||||
Fixture f(22050);
|
||||
SetLowPass(f, 6833.0f);
|
||||
const auto& filter = f.voice.filters[0];
|
||||
// Rear filtering operates at the 48 kHz system rate, not the PCM source rate.
|
||||
const double w = (2.0 * std::numbers::pi) * 6833 / 48000;
|
||||
const double expected = (1 - std::cos(w)) / (2 * (1 + std::sin(w) / (2 * double(0.70710678f))));
|
||||
Check(std::abs(filter.b0 - expected) < 1e-10, "rear filter used source sample rate");
|
||||
SetLowPass(f, 48000);
|
||||
const int16_t pcm[] = {16384, 16384};
|
||||
f.Queue(pcm, 2, 0);
|
||||
f.Render(2);
|
||||
Check(f.voice.samples[0] == 0.5f && f.voice.samples[1] == 0.5f,
|
||||
"above-Nyquist cutoff was unstable");
|
||||
}
|
||||
|
||||
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};
|
||||
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&f.voice),
|
||||
reinterpret_cast<const Ngs2VoiceParamHeader*>(words));
|
||||
Check(f.voice.filters.size() == 1 && f.voice.filters[0].enabled,
|
||||
"FCQ filter command was not recognized");
|
||||
const double omega = (2.0 * std::numbers::pi) * 6833.0 / 48000.0;
|
||||
const double q = static_cast<double>(std::bit_cast<float>(0x3fd55555u));
|
||||
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.
|
||||
Fixture wide(48000, 33);
|
||||
wide.rack.option.sampler.max_filters = 8;
|
||||
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&wide.voice),
|
||||
reinterpret_cast<const Ngs2VoiceParamHeader*>(words));
|
||||
const std::vector<int16_t> pcm(33, 16384);
|
||||
wide.Queue(pcm.data(), 1, 1);
|
||||
wide.Render(1);
|
||||
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;
|
||||
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&wide.voice),
|
||||
reinterpret_cast<const Ngs2VoiceParamHeader*>(words));
|
||||
Check(!wide.voice.filters[0].enabled && wide.voice.filters[0].history.empty() &&
|
||||
!wide.voice.filters[0].warned,
|
||||
"filter OFF was treated as an unsupported FCQ configuration");
|
||||
wide.Queue(pcm.data(), 1, 1);
|
||||
wide.Render(1);
|
||||
Check(wide.voice.samples[1] == 0.5f, "filter OFF did not bypass audio");
|
||||
SetLowPass(wide, 0);
|
||||
wide.Queue(pcm.data(), 1, 1);
|
||||
wide.Render(1);
|
||||
Check(wide.voice.samples[1] == 0.0f, "zero-Hz low-pass leaked source audio");
|
||||
}
|
||||
|
||||
struct LoopCallbacks {
|
||||
std::vector<std::pair<uint32_t, uint32_t>> events;
|
||||
bool pause = false;
|
||||
};
|
||||
|
||||
void KYTY_SYSV_ABI RecordLoopCallback(const Ngs2VoiceCallbackInfo* info) {
|
||||
auto& log = *reinterpret_cast<LoopCallbacks*>(info->data);
|
||||
log.events.emplace_back(info->flag, info->repeats);
|
||||
Check(info->user == 123 && info->attributes == 0, "loop callback metadata changed");
|
||||
if (log.pause && info->flag == 2) {
|
||||
reinterpret_cast<Ngs2VoiceInternal*>(info->voice)->SetEvent(16);
|
||||
}
|
||||
}
|
||||
|
||||
void TestLoopCallbacksAndExit() {
|
||||
const int16_t pcm[] = {8192, 16384, 24576};
|
||||
Fixture f(48000);
|
||||
LoopCallbacks log;
|
||||
f.voice.callback = reinterpret_cast<uintptr_t>(&RecordLoopCallback);
|
||||
f.voice.callback_data = reinterpret_cast<uintptr_t>(&log);
|
||||
f.voice.callback_flags = 3;
|
||||
f.Queue(pcm, 2, 0, 2);
|
||||
f.Render(6);
|
||||
Check(log.events == std::vector<std::pair<uint32_t, uint32_t>> {{2, 1}, {2, 2}, {1, 2}},
|
||||
"loop and completion callbacks lost the completed repeat count");
|
||||
Check(f.voice.blocks.empty(), "finite loop did not complete");
|
||||
|
||||
Fixture active(48000);
|
||||
log.events.clear();
|
||||
log.pause = true;
|
||||
active.voice.callback = reinterpret_cast<uintptr_t>(&RecordLoopCallback);
|
||||
active.voice.callback_data = reinterpret_cast<uintptr_t>(&log);
|
||||
active.voice.callback_flags = 2;
|
||||
active.Queue(pcm, 2, 1, UINT32_MAX);
|
||||
active.Queue(pcm + 2, 1, 0);
|
||||
active.Render(6);
|
||||
Check(log.events.size() == 1 && active.voice.decoded_samples == 2 &&
|
||||
active.voice.blocks.front().cursor == 0 && active.voice.samples[2] == 0,
|
||||
"loop callback pause consumed another iteration");
|
||||
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.voice.blocks.empty() && log.events.size() == 1,
|
||||
"exiting infinite loop lost its final iteration or ignored callback flags");
|
||||
|
||||
Fixture pending(48000);
|
||||
pending.Queue(pcm, 1, 1);
|
||||
pending.Queue(pcm + 1, 1, 1, UINT32_MAX);
|
||||
pending.Queue(pcm + 2, 1, 0);
|
||||
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&pending.voice), &exit_loop);
|
||||
pending.Render(3);
|
||||
Check(pending.voice.samples == std::vector<float> {0.25f, 0.5f, 0.75f} &&
|
||||
pending.voice.blocks.empty(),
|
||||
"exiting a pending loop skipped its first iteration");
|
||||
Ngs2VoiceControl(reinterpret_cast<uintptr_t>(&pending.voice), &exit_loop);
|
||||
}
|
||||
|
||||
void TestWaveformReadAddress() {
|
||||
Fixture f(48000, 2);
|
||||
const int16_t pcm[] = {-100, -100, 8192, 4096, 16384, 8192};
|
||||
f.Queue(pcm, 2, 1, 1, 1);
|
||||
const auto next_data = [&]() {
|
||||
Ngs2SamplerVoiceState state {};
|
||||
Ngs2VoiceGetState(reinterpret_cast<uintptr_t>(&f.voice), &state.voice_state, sizeof(state));
|
||||
return state.waveform_data;
|
||||
};
|
||||
Check(next_data() == pcm + 2, "read address ignored skipped source samples");
|
||||
f.Render(1);
|
||||
Check(next_data() == pcm + 4, "read address did not advance by a stereo frame");
|
||||
f.Render(1);
|
||||
Check(next_data() == pcm + 2, "read address did not wrap at a loop boundary");
|
||||
f.Render(2);
|
||||
Check(f.voice.blocks.empty() && next_data() == pcm + 6,
|
||||
"starved read address did not preserve the consumed waveform end");
|
||||
f.Queue(pcm + 2, 2, 0);
|
||||
Check(next_data() == pcm + 2, "refilled read address retained the previous waveform end");
|
||||
f.Render(2);
|
||||
Check(next_data() == pcm + 6, "completed read address did not preserve waveform end");
|
||||
f.voice.SetupSampler({0x12, 2, 48000});
|
||||
Check(next_data() == nullptr, "setup retained an old waveform address");
|
||||
}
|
||||
|
||||
struct CallbackEvent {
|
||||
Ngs2VoiceInternal* voice;
|
||||
uint32_t event;
|
||||
uint32_t calls = 0;
|
||||
};
|
||||
|
||||
void KYTY_SYSV_ABI ApplyCallbackEvent(const Ngs2VoiceCallbackInfo* info) {
|
||||
auto& context = *reinterpret_cast<CallbackEvent*>(info->data);
|
||||
if (++context.calls == 1) {
|
||||
context.voice->SetEvent(context.event);
|
||||
}
|
||||
}
|
||||
|
||||
void TestCallbackStopsConsumption() {
|
||||
for (auto rack: {Ngs2RackType::Sampler, Ngs2RackType::CustomSampler}) {
|
||||
for (uint32_t event: {2u, 4u, 8u, 16u}) {
|
||||
Fixture f(48000, 1, rack);
|
||||
const int16_t pcm[] = {8192, 16384};
|
||||
f.Queue(pcm, 1, 1);
|
||||
f.Queue(pcm + 1, 1, 1);
|
||||
CallbackEvent context {&f.voice, event};
|
||||
f.voice.callback = reinterpret_cast<uintptr_t>(&ApplyCallbackEvent);
|
||||
f.voice.callback_data = reinterpret_cast<uintptr_t>(&context);
|
||||
f.voice.callback_flags = 1;
|
||||
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 &&
|
||||
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,
|
||||
"callback pause failed to preserve queued audio for resume");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TestCallbackPauseDuringLargeStep() {
|
||||
Fixture f(48000);
|
||||
const int16_t pcm[] = {8192, 16384, 24576, 4096};
|
||||
f.Queue(pcm, 1, 1);
|
||||
f.Queue(pcm + 1, 1, 1);
|
||||
f.Queue(pcm + 2, 2, 1);
|
||||
f.voice.sample_step = uint64_t(120000) << 32u;
|
||||
CallbackEvent context {&f.voice, 16};
|
||||
f.voice.callback = reinterpret_cast<uintptr_t>(&ApplyCallbackEvent);
|
||||
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),
|
||||
"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,
|
||||
"resume lost fractional position or read past the next short block");
|
||||
}
|
||||
|
||||
void TestUpstreamCustomSamplerControls() {
|
||||
Fixture f(24000, 1, Ngs2RackType::CustomSampler);
|
||||
const int16_t pcm[] = {8192, 16384, 24576, 4096};
|
||||
f.Queue(pcm, 4, 1);
|
||||
f.Render(1);
|
||||
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,
|
||||
"replacement blocks retained old data or fractional position");
|
||||
struct Pitch {
|
||||
Ngs2VoiceParamHeader header {16, 0, 0x40010005};
|
||||
float ratio = 0.0f;
|
||||
uint32_t reserved = 0;
|
||||
} pitch;
|
||||
const auto handle = reinterpret_cast<uintptr_t>(&f.voice);
|
||||
Ngs2VoiceControl(handle, &pitch.header);
|
||||
f.Render(2);
|
||||
Check(f.voice.samples[0] == 0.4375f && f.voice.samples[1] == 0.4375f,
|
||||
"zero custom sampler pitch advanced playback");
|
||||
pitch.ratio = 4.0f;
|
||||
Ngs2VoiceControl(handle, &pitch.header);
|
||||
f.Render(1);
|
||||
Check(f.voice.blocks.empty(), "custom sampler pitch did not advance playback");
|
||||
f.rack.option.custom_sampler.custom_rack_option.state_size =
|
||||
sizeof(Ngs2CustomSamplerVoiceState);
|
||||
Ngs2CustomSamplerVoiceState state {};
|
||||
Check(Ngs2VoiceGetState(handle, &state.voice_state, sizeof(state)) == OK &&
|
||||
state.voice_state.state_flags == Ngs2GetStateFlags(&f.voice),
|
||||
"custom sampler state layout regressed");
|
||||
}
|
||||
|
||||
void TestFilterTailDuringStarvation() {
|
||||
Fixture reference(48000), stream(48000);
|
||||
SetLowPass(reference);
|
||||
SetLowPass(stream);
|
||||
std::vector<int16_t> pcm(128, 0);
|
||||
pcm[0] = pcm[64] = 16384;
|
||||
reference.Queue(pcm.data(), 128, 1);
|
||||
reference.Render(128);
|
||||
stream.Queue(pcm.data(), 1, 1);
|
||||
stream.Render(1);
|
||||
Check(stream.voice.samples[0] == reference.voice.samples[0], "initial filter sample differs");
|
||||
stream.Render(63);
|
||||
Check(stream.voice.has_samples, "starved filter tail was not available for routing");
|
||||
for (size_t i = 0; i < 63; ++i) {
|
||||
Check(std::abs(stream.voice.samples[i] - reference.voice.samples[i + 1]) < 1e-7f,
|
||||
"filter tail changed at a starvation/grain boundary");
|
||||
}
|
||||
stream.Queue(pcm.data() + 64, 64, 1);
|
||||
stream.Render(64);
|
||||
for (size_t i = 0; i < 64; ++i) {
|
||||
Check(std::abs(stream.voice.samples[i] - reference.voice.samples[i + 64]) < 1e-7f,
|
||||
"refilled stream reused stale filter history");
|
||||
}
|
||||
stream.Render(8192);
|
||||
stream.Render(32);
|
||||
Check(!stream.voice.has_samples && !stream.voice.filters[0].HasHistory(),
|
||||
"fully decayed filter kept a silent stream active");
|
||||
}
|
||||
|
||||
void TestResamplingPhaseAfterStarvation() {
|
||||
for (auto rack: {Ngs2RackType::Sampler, Ngs2RackType::CustomSampler}) {
|
||||
for (uint32_t rate: {44100u, 96000u}) {
|
||||
Fixture reference(rate, 1, rack), stream(rate, 1, rack);
|
||||
const int16_t pcm[] = {0, 0, 16384, 8192};
|
||||
const uint32_t frames = rate == 44100 ? 2 : 1;
|
||||
reference.Queue(pcm, 4, 0);
|
||||
stream.Queue(pcm, 1, 1);
|
||||
reference.Render(frames);
|
||||
stream.Render(frames);
|
||||
Check(stream.voice.blocks.empty() && stream.voice.state == Ngs2VoicePlayState::Playing,
|
||||
"open stream did not enter starvation");
|
||||
stream.Render(8);
|
||||
Check(!stream.voice.has_samples, "starved stream produced audio without a filter");
|
||||
stream.Queue(pcm + 1, 3, 0);
|
||||
reference.Render(1);
|
||||
stream.Render(1);
|
||||
Check(std::abs(stream.voice.samples[0] - reference.voice.samples[0]) < 1e-7f,
|
||||
"stream refill lost resampling phase");
|
||||
Check(stream.voice.decoded_samples == reference.voice.decoded_samples,
|
||||
"stream refill lost pending source advancement");
|
||||
stream.voice.SetupSampler({0x12, 1, rate});
|
||||
Check(stream.voice.sample_phase == 0, "sampler setup retained old resampling phase");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TestFiniteFilterTail() {
|
||||
Fixture whole(48000), split(48000);
|
||||
SetLowPass(whole);
|
||||
SetLowPass(split);
|
||||
const int16_t impulse = 16384;
|
||||
whole.Queue(&impulse, 1, 0);
|
||||
split.Queue(&impulse, 1, 0);
|
||||
whole.Render(256);
|
||||
for (size_t i = 0; i < whole.voice.samples.size(); ++i) {
|
||||
split.Render(1);
|
||||
Check(std::abs(split.voice.samples[0] - whole.voice.samples[i]) < 1e-7f,
|
||||
"finite filter tail changed with render grain size");
|
||||
}
|
||||
Check(split.voice.state == Ngs2VoicePlayState::Playing,
|
||||
"finite voice stopped before its filter tail decayed");
|
||||
split.Render(8192);
|
||||
Check(split.voice.state == Ngs2VoicePlayState::Empty,
|
||||
"finite voice remained active after its filter tail decayed");
|
||||
split.Render(32);
|
||||
Check(!split.voice.has_samples, "completed finite filter tail still produced samples");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
TestResamplingPhaseAfterStarvation();
|
||||
TestSamplerReuseResetsRouting();
|
||||
TestLoopCallbacksAndExit();
|
||||
TestWaveformReadAddress();
|
||||
TestFilterTailDuringStarvation();
|
||||
TestFiniteFilterTail();
|
||||
TestLowPassResponse();
|
||||
TestFilterHistoryAndChannels();
|
||||
TestFilterUsesOutputRate();
|
||||
TestFilterControlLayout();
|
||||
TestUpstreamCustomSamplerControls();
|
||||
TestCallbackStopsConsumption();
|
||||
TestCallbackPauseDuringLargeStep();
|
||||
TestStreamingResample();
|
||||
TestPitchLoopAndSkip();
|
||||
TestStarvationAndPause();
|
||||
TestMonoRateAndRouting();
|
||||
TestCustomPcmStillPlays();
|
||||
TestPlayStateBeforeRender();
|
||||
TestStatePublication();
|
||||
TestStopThenPlayBeforeRender();
|
||||
std::puts("Ngs2SamplerTests: all cases passed");
|
||||
}
|
||||
@@ -2480,6 +2480,22 @@ public:
|
||||
cb_db_release_label == 0 &&
|
||||
gpu_scheduler.CurrentTick() == cb_db_tick;
|
||||
|
||||
for (const auto gcr : {0u, 1u << 9u}) {
|
||||
const auto before = Sync::ReadReferenceClock();
|
||||
auto timestamp =
|
||||
make_release_mem(3, 0, &cb_db_release_label, UINT64_MAX, 0x14u, gcr);
|
||||
Pm4Execution timestamp_execution;
|
||||
const auto timestamp_result =
|
||||
processor->Process(timestamp_execution, timestamp);
|
||||
Require("GpuCommandLane", "timestamp write",
|
||||
timestamp_result == Pm4ProcessResult::Complete &&
|
||||
cb_db_release_label >= before &&
|
||||
cb_db_release_label <= Sync::ReadReferenceClock() &&
|
||||
gpu_scheduler.CurrentTick() == cb_db_tick,
|
||||
"timestamp write failed, returned an invalid time, or "
|
||||
"submitted extra GPU work");
|
||||
}
|
||||
|
||||
auto gds_interrupt_only =
|
||||
make_release_mem(5, 1, &interrupt_only_gds_label, 1ull << 16u);
|
||||
Pm4Execution gds_interrupt_execution;
|
||||
@@ -2521,6 +2537,14 @@ public:
|
||||
gpu.SendCommandSync([&] {
|
||||
processor->BufferInit();
|
||||
|
||||
const auto clock_before = Sync::ReadReferenceClock();
|
||||
processor->WriteAtEndOfPipe64(0, 0, 0x04, 0x38, 5, 4,
|
||||
&release_label, UINT64_MAX, 2);
|
||||
Require("GpuCommandLane", "EOP reference clock with interrupt",
|
||||
release_label >= clock_before &&
|
||||
release_label <= Sync::ReadReferenceClock(),
|
||||
"clock write with writeback and interrupt lost its data");
|
||||
|
||||
auto immediate = make_release_mem(1, 0, &release_label, 0x11223344u);
|
||||
Pm4Execution immediate_execution;
|
||||
const auto immediate_tick = gpu_scheduler.CurrentTick();
|
||||
|
||||
Reference in New Issue
Block a user