feat(libSceNgs2.native): sampler voice low-pass filter
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
109a3281f8
commit
819c045327
|
|
@ -25,7 +25,7 @@ if(MINGW)
|
|||
endif()
|
||||
|
||||
if(BUILD_TESTING)
|
||||
foreach(ngs2Test IN ITEMS Render Atrac9)
|
||||
foreach(ngs2Test IN ITEMS Render Atrac9 Filter)
|
||||
string(TOLOWER ${ngs2Test} testName)
|
||||
add_test_executable(ngs2_${testName}_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/Ngs2${ngs2Test}.cpp)
|
||||
target_include_directories(ngs2_${testName}_tests PRIVATE ${LIBS_INCLUDE_DIR} ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src)
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ static constexpr std::uint32_t SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP = 0x10000000;
|
|||
static constexpr std::uint32_t SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS = 0x10000001;
|
||||
static constexpr std::uint32_t SCE_NGS2_SAMPLER_VOICE_PARAM_EXIT_LOOP = 0x10000004;
|
||||
static constexpr std::uint32_t SCE_NGS2_SAMPLER_VOICE_PARAM_PITCH = 0x10000005;
|
||||
static constexpr std::uint32_t SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER = 0x1000000a;
|
||||
static constexpr std::uint32_t SCE_NGS2_SUBMIXER_VOICE_PARAM_SETUP = 0x20000000;
|
||||
static constexpr std::uint32_t SCE_NGS2_MASTERING_VOICE_PARAM_SETUP = 0x30000000;
|
||||
static constexpr std::uint32_t SCE_NGS2_MASTERING_VOICE_PARAM_OUTPUT = 0x30000005;
|
||||
|
|
@ -254,6 +255,19 @@ struct Ngs2SamplerVoicePitchParam {
|
|||
float ratio;
|
||||
};
|
||||
|
||||
struct Ngs2SamplerVoiceFilterParam {
|
||||
Ngs2VoiceParamHeader header;
|
||||
std::uint32_t index;
|
||||
std::uint32_t location;
|
||||
std::uint32_t type;
|
||||
std::uint64_t channel_mask;
|
||||
float frequency;
|
||||
float q;
|
||||
float level;
|
||||
std::uint32_t reserved[3];
|
||||
};
|
||||
static_assert(sizeof(Ngs2SamplerVoiceFilterParam) == 56);
|
||||
|
||||
struct Ngs2SubmixerVoiceSetupParam {
|
||||
Ngs2VoiceParamHeader header;
|
||||
std::uint32_t num_io_channels;
|
||||
|
|
|
|||
|
|
@ -47,6 +47,24 @@ struct Ngs2Atrac9 {
|
|||
|
||||
struct Ngs2Voice;
|
||||
|
||||
struct Ngs2FilterHistory {
|
||||
double x1 = 0.0;
|
||||
double x2 = 0.0;
|
||||
double y1 = 0.0;
|
||||
double y2 = 0.0;
|
||||
};
|
||||
|
||||
struct Ngs2Filter {
|
||||
bool enabled = false;
|
||||
std::uint64_t bypassMask = 0;
|
||||
double b0 = 1.0;
|
||||
double b1 = 0.0;
|
||||
double b2 = 0.0;
|
||||
double a1 = 0.0;
|
||||
double a2 = 0.0;
|
||||
std::vector<Ngs2FilterHistory> history;
|
||||
};
|
||||
|
||||
struct Ngs2Port {
|
||||
Ngs2Voice* dest = nullptr;
|
||||
float volume = 1.0f;
|
||||
|
|
@ -73,6 +91,7 @@ struct Ngs2Voice {
|
|||
std::uint32_t callbackFlags = 0;
|
||||
std::vector<Ngs2Port> ports;
|
||||
std::vector<std::vector<float>> matrices;
|
||||
std::vector<Ngs2Filter> filters;
|
||||
std::uint32_t outputId = 0;
|
||||
std::vector<float> samples;
|
||||
bool rendering = false;
|
||||
|
|
@ -88,6 +107,7 @@ struct Ngs2Rack {
|
|||
Ngs2System* system = nullptr;
|
||||
std::uint32_t rackId = 0;
|
||||
std::uint32_t maxChannels = 0;
|
||||
std::uint32_t maxFilters = 0;
|
||||
Ngs2ContextBufferInfo bufferInfo{};
|
||||
Ngs2BufferAllocator allocator{};
|
||||
std::vector<Ngs2Voice> voices;
|
||||
|
|
|
|||
|
|
@ -93,6 +93,7 @@ static int CreateRack(Ngs2Handle systemHandle, std::uint32_t rackId, const RackO
|
|||
rack->system = system;
|
||||
rack->rackId = rackId;
|
||||
rack->maxChannels = RackMaxChannels(rackId, options);
|
||||
rack->maxFilters = rackId == SCE_NGS2_RACK_ID_SAMPLER ? options.sampler.max_filters : 0;
|
||||
rack->bufferInfo = bufferInfo;
|
||||
rack->allocator = allocator;
|
||||
rack->voices.resize(options.common.max_voices);
|
||||
|
|
|
|||
|
|
@ -99,7 +99,46 @@ static void ConsumeSamples(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t
|
|||
voice.phase &= PHASE_FRACTION;
|
||||
}
|
||||
voice.hasSamples = written != 0;
|
||||
if (voice.state == Ngs2PlayState::Playing && voice.blocks.empty() && !voice.acceptsBlocks) voice.state = Ngs2PlayState::Empty;
|
||||
}
|
||||
|
||||
static bool Bypasses(const Ngs2Filter& filter, std::uint32_t channel) {
|
||||
return channel < 64 && (filter.bypassMask & (std::uint64_t{1} << channel)) != 0;
|
||||
}
|
||||
|
||||
static bool HasHistory(const Ngs2Filter& filter) {
|
||||
if (!filter.enabled) return false;
|
||||
for (std::uint32_t channel = 0; channel < filter.history.size(); channel++) {
|
||||
const auto& h = filter.history[channel];
|
||||
if (!Bypasses(filter, channel) && (h.x1 != 0.0 || h.x2 != 0.0 || h.y1 != 0.0 || h.y2 != 0.0)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void ApplyFilter(Ngs2Voice& voice, Ngs2Filter& filter, std::uint32_t grain) {
|
||||
for (std::uint32_t channel = 0; channel < voice.channels; channel++) {
|
||||
if (Bypasses(filter, channel)) continue;
|
||||
auto& h = filter.history[channel];
|
||||
for (std::uint32_t i = 0; i < grain; i++) {
|
||||
auto& sample = voice.samples[channel * grain + i];
|
||||
const double x = sample;
|
||||
double y = filter.b0 * x + filter.b1 * h.x1 + filter.b2 * h.x2 - filter.a1 * h.y1 - filter.a2 * h.y2;
|
||||
if (std::abs(y) < 1e-20) y = 0.0;
|
||||
h = {x, h.x1, y, h.y1};
|
||||
sample = static_cast<float>(y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void RenderSampler(Ngs2Voice& voice, std::uint32_t grain, std::uint32_t systemRate) {
|
||||
ConsumeSamples(voice, grain, systemRate);
|
||||
for (auto& filter : voice.filters) {
|
||||
if (!filter.enabled || (!voice.hasSamples && !HasHistory(filter))) continue;
|
||||
ApplyFilter(voice, filter, grain);
|
||||
voice.hasSamples = true;
|
||||
}
|
||||
if (voice.state == Ngs2PlayState::Playing && voice.blocks.empty() && !voice.acceptsBlocks && std::none_of(voice.filters.begin(), voice.filters.end(), HasHistory)) {
|
||||
voice.state = Ngs2PlayState::Empty;
|
||||
}
|
||||
}
|
||||
|
||||
static void RenderVoice(Ngs2Voice& voice, const std::vector<Ngs2Voice*>& voices, std::uint32_t grain, std::uint32_t systemRate);
|
||||
|
|
@ -135,7 +174,7 @@ static void RenderVoice(Ngs2Voice& voice, const std::vector<Ngs2Voice*>& voices,
|
|||
voice.samples.assign(static_cast<std::size_t>(grain) * voice.channels, 0.0f);
|
||||
voice.hasSamples = false;
|
||||
if (voice.state == Ngs2PlayState::Playing && voice.channels != 0) {
|
||||
if (voice.rack->rackId == SCE_NGS2_RACK_ID_SAMPLER) ConsumeSamples(voice, grain, systemRate);
|
||||
if (voice.rack->rackId == SCE_NGS2_RACK_ID_SAMPLER) RenderSampler(voice, grain, systemRate);
|
||||
else MixInputs(voice, voices, grain, systemRate);
|
||||
}
|
||||
voice.rendering = false;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <numbers>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
|
|
@ -47,6 +48,7 @@ void Ngs2Voice::ResetSetup() {
|
|||
SetEvent(SCE_NGS2_VOICE_EVENT_STOP_IMM);
|
||||
std::fill(ports.begin(), ports.end(), Ngs2Port{});
|
||||
for (auto& matrix : matrices) matrix.clear();
|
||||
filters.clear();
|
||||
channels = 0;
|
||||
sampleRate = 0;
|
||||
waveformType = 0;
|
||||
|
|
@ -178,6 +180,40 @@ static void AddWaveformBlocks(Ngs2Voice& voice, const Ngs2SamplerVoiceWaveformBl
|
|||
}
|
||||
}
|
||||
|
||||
static void SetFilter(Ngs2Voice& voice, const Ngs2SamplerVoiceFilterParam& param) {
|
||||
if (param.index >= voice.rack->maxFilters) APS5_INVALID_ARG_EX;
|
||||
if (voice.filters.size() <= param.index) voice.filters.resize(param.index + 1);
|
||||
auto& filter = voice.filters[param.index];
|
||||
if (param.type == 0) {
|
||||
filter = {};
|
||||
return;
|
||||
}
|
||||
if (param.location != 1 || param.type != 1) {
|
||||
throw std::runtime_error("NGS2: sampler filter location " + std::to_string(param.location) + " type " + std::to_string(param.type) + " is not implemented");
|
||||
}
|
||||
if (!std::isfinite(param.frequency) || param.frequency < 0.0f || !std::isfinite(param.q) || param.q <= 0.0f || !std::isfinite(param.level) || param.level < 0.0f) {
|
||||
APS5_INVALID_ARG_EX;
|
||||
}
|
||||
const double rate = voice.rack->system->option.sample_rate;
|
||||
filter.enabled = true;
|
||||
filter.bypassMask = param.channel_mask;
|
||||
filter.history.resize(voice.channels);
|
||||
if (param.frequency == 0.0f || param.frequency >= rate * 0.5) {
|
||||
filter.b0 = param.frequency == 0.0f ? 0.0 : param.level;
|
||||
filter.b1 = filter.b2 = filter.a1 = filter.a2 = 0.0;
|
||||
return;
|
||||
}
|
||||
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 inverseA0 = 1.0 / (1.0 + alpha);
|
||||
filter.b0 = (1.0 - cosine) * 0.5 * inverseA0 * param.level;
|
||||
filter.b1 = 2.0 * filter.b0;
|
||||
filter.b2 = filter.b0;
|
||||
filter.a1 = -2.0 * cosine * inverseA0;
|
||||
filter.a2 = (1.0 - alpha) * inverseA0;
|
||||
}
|
||||
|
||||
static void ApplySamplerParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& param) {
|
||||
switch (param.id) {
|
||||
case SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP: SetupSampler(voice, ParamAs<Ngs2SamplerVoiceSetupParam>(param).format); break;
|
||||
|
|
@ -193,6 +229,7 @@ static void ApplySamplerParam(Ngs2Voice& voice, const Ngs2VoiceParamHeader& para
|
|||
voice.pitch = ratio;
|
||||
break;
|
||||
}
|
||||
case SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER: SetFilter(voice, ParamAs<Ngs2SamplerVoiceFilterParam>(param)); break;
|
||||
default: throw std::runtime_error("NGS2: sampler voice param " + Ngs2Hex(param.id) + " is not implemented");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,182 @@
|
|||
#include "Ngs2Test.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <numbers>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
struct Biquad {
|
||||
double b0, b1, b2, a1, a2;
|
||||
double x1 = 0.0, x2 = 0.0, y1 = 0.0, y2 = 0.0;
|
||||
|
||||
Biquad(double frequency, double q, double level, double rate) {
|
||||
const double w = 2.0 * std::numbers::pi * frequency / rate;
|
||||
const double alpha = std::sin(w) / (2.0 * q);
|
||||
const double a0 = 1.0 + alpha;
|
||||
b0 = (1.0 - std::cos(w)) / 2.0 / a0 * level;
|
||||
b1 = (1.0 - std::cos(w)) / a0 * level;
|
||||
b2 = b0;
|
||||
a1 = -2.0 * std::cos(w) / a0;
|
||||
a2 = (1.0 - alpha) / a0;
|
||||
}
|
||||
|
||||
float Next(double x) {
|
||||
const double y = b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
|
||||
x2 = x1;
|
||||
x1 = x;
|
||||
y2 = y1;
|
||||
y1 = y;
|
||||
return static_cast<float>(y);
|
||||
}
|
||||
};
|
||||
|
||||
static Ngs2SamplerVoiceFilterParam Filter(std::uint32_t index, std::uint32_t location, std::uint32_t type, std::uint64_t mask, float frequency, float q, float level) {
|
||||
Ngs2SamplerVoiceFilterParam param{};
|
||||
param.header = {sizeof(param), 0, SCE_NGS2_SAMPLER_VOICE_PARAM_FILTER};
|
||||
param.index = index;
|
||||
param.location = location;
|
||||
param.type = type;
|
||||
param.channel_mask = mask;
|
||||
param.frequency = frequency;
|
||||
param.q = q;
|
||||
param.level = level;
|
||||
return param;
|
||||
}
|
||||
|
||||
static void SetFilter(uintptr_t voice, const Ngs2SamplerVoiceFilterParam& param) {
|
||||
Require(sceNgs2VoiceControl(voice, ¶m.header) == SCE_NGS2_OK);
|
||||
}
|
||||
|
||||
static uintptr_t Sampler(uintptr_t system, const std::vector<std::int16_t>& pcm, std::uint32_t channels, std::uint32_t rate) {
|
||||
const auto voice = Voice(CreateRack(system, SCE_NGS2_RACK_ID_SAMPLER));
|
||||
Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, channels, rate, 0, 0, 0}});
|
||||
const auto frames = static_cast<std::uint32_t>(pcm.size() / channels);
|
||||
const Ngs2WaveformBlock block{0, pcm.size() * sizeof(std::int16_t), 0, 0, frames, 0, 0};
|
||||
Control(voice, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block});
|
||||
return voice;
|
||||
}
|
||||
|
||||
static std::vector<float> RenderGrain(uintptr_t system, std::uint32_t channels) {
|
||||
std::vector<float> out(Grain * channels, -1.0f);
|
||||
const Ngs2RenderBufferInfo info{out.data(), out.size() * sizeof(float), SCE_NGS2_WAVEFORM_TYPE_PCM_F32L, channels};
|
||||
Require(sceNgs2SystemRender(system, &info, 1) == SCE_NGS2_OK);
|
||||
return out;
|
||||
}
|
||||
|
||||
static void TestLowPassAndTail() {
|
||||
const auto system = CreateSystem();
|
||||
const auto master = Mastering(system, 1);
|
||||
std::vector<std::int16_t> pcm(Grain * 2, 0);
|
||||
pcm[0] = 16384;
|
||||
pcm[5] = -8192;
|
||||
const auto sampler = Sampler(system, pcm, 1, 48000);
|
||||
Patch(sampler, master);
|
||||
const auto param = Filter(7, 1, 1, 0, 1000.0f, 0.70710678f, 0.75f);
|
||||
SetFilter(sampler, param);
|
||||
Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY);
|
||||
|
||||
Biquad reference(1000.0, static_cast<double>(0.70710678f), 0.75, 48000.0);
|
||||
for (std::uint32_t grain = 0; grain < 4; grain++) {
|
||||
const auto out = RenderGrain(system, 1);
|
||||
for (std::uint32_t i = 0; i < Grain; i++) {
|
||||
const auto frame = grain * Grain + i;
|
||||
const float expected = reference.Next(frame < pcm.size() ? pcm[frame] / 32768.0 : 0.0);
|
||||
Require(std::abs(out[i] - expected) <= 1e-7f);
|
||||
}
|
||||
Require(out[Grain - 1] != 0.0f);
|
||||
Require(Flags(sampler) == (SCE_NGS2_VOICE_STATE_FLAG_INUSE | SCE_NGS2_VOICE_STATE_FLAG_PLAYING));
|
||||
SetFilter(sampler, param);
|
||||
}
|
||||
|
||||
SetFilter(sampler, Filter(7, 0, 0, 0, NAN, 0.0f, -1.0f));
|
||||
for (float sample : RenderGrain(system, 1)) Require(sample == 0.0f);
|
||||
Require(Flags(sampler) == 0);
|
||||
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
|
||||
}
|
||||
|
||||
static void TestBypassMaskAndGain() {
|
||||
const auto system = CreateSystem();
|
||||
const auto master = Mastering(system, 2);
|
||||
std::vector<std::int16_t> pcm;
|
||||
for (std::uint32_t i = 0; i < Grain; i++) {
|
||||
pcm.push_back(static_cast<std::int16_t>(i * 1000));
|
||||
pcm.push_back(static_cast<std::int16_t>(-4000 + i * 700));
|
||||
}
|
||||
const auto sampler = Sampler(system, pcm, 2, 48000);
|
||||
Patch(sampler, master);
|
||||
SetFilter(sampler, Filter(0, 1, 1, 1, 24000.0f, 1.0f, 0.5f));
|
||||
Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY);
|
||||
|
||||
auto out = RenderGrain(system, 2);
|
||||
for (std::uint32_t i = 0; i < Grain; i++) {
|
||||
Require(out[i * 2] == pcm[i * 2] / 32768.0f);
|
||||
Require(out[i * 2 + 1] == pcm[i * 2 + 1] / 32768.0f * 0.5f);
|
||||
}
|
||||
Require(Flags(sampler) == (SCE_NGS2_VOICE_STATE_FLAG_INUSE | SCE_NGS2_VOICE_STATE_FLAG_PLAYING));
|
||||
out = RenderGrain(system, 2);
|
||||
for (float sample : out) Require(sample == 0.0f);
|
||||
Require(Flags(sampler) == 0);
|
||||
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
|
||||
}
|
||||
|
||||
static void TestSystemRateAndZeroCutoff() {
|
||||
const auto system = CreateSystem();
|
||||
const auto master = Mastering(system, 1);
|
||||
const std::vector<std::int16_t> pcm(Grain * 2, 8192);
|
||||
const auto sampler = Sampler(system, pcm, 1, 24000);
|
||||
Patch(sampler, master);
|
||||
SetFilter(sampler, Filter(0, 1, 1, 0, 18000.0f, 0.70710678f, 1.0f));
|
||||
SetFilter(sampler, Filter(1, 1, 1, 0, 30000.0f, 0.70710678f, 1.0f));
|
||||
Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY);
|
||||
Biquad reference(18000.0, static_cast<double>(0.70710678f), 1.0, 48000.0);
|
||||
const auto out = RenderGrain(system, 1);
|
||||
for (std::uint32_t i = 0; i < Grain; i++) Require(std::abs(out[i] - reference.Next(0.25)) <= 1e-7f);
|
||||
|
||||
SetFilter(sampler, Filter(1, 1, 1, 0, 0.0f, 0.70710678f, 1.0f));
|
||||
for (float sample : RenderGrain(system, 1)) Require(sample == 0.0f);
|
||||
|
||||
Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_SETUP, Ngs2SamplerVoiceSetupParam{{}, {SCE_NGS2_WAVEFORM_TYPE_PCM_I16L, 1, 48000, 0, 0, 0}});
|
||||
const Ngs2WaveformBlock block{0, pcm.size() * sizeof(std::int16_t), 0, 0, static_cast<std::uint32_t>(pcm.size()), 0, 0};
|
||||
Control(sampler, SCE_NGS2_SAMPLER_VOICE_PARAM_ADD_WAVEFORM_BLOCKS, Ngs2SamplerVoiceWaveformBlocksParam{{}, pcm.data(), 0, 1, &block});
|
||||
Patch(sampler, master);
|
||||
Event(sampler, SCE_NGS2_VOICE_EVENT_PLAY);
|
||||
for (float sample : RenderGrain(system, 1)) Require(sample == 0.25f);
|
||||
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
|
||||
}
|
||||
|
||||
template <typename TError>
|
||||
static void RequireRejected(uintptr_t voice, const Ngs2SamplerVoiceFilterParam& param) {
|
||||
bool rejected = false;
|
||||
try {
|
||||
sceNgs2VoiceControl(voice, ¶m.header);
|
||||
} catch (const TError&) {
|
||||
rejected = true;
|
||||
}
|
||||
Require(rejected);
|
||||
}
|
||||
|
||||
static void TestRejectedParams() {
|
||||
const auto system = CreateSystem();
|
||||
const auto sampler = Sampler(system, std::vector<std::int16_t>(Grain, 0), 1, 48000);
|
||||
RequireRejected<std::invalid_argument>(sampler, Filter(8, 1, 1, 0, 1000.0f, 1.0f, 1.0f));
|
||||
RequireRejected<std::runtime_error>(sampler, Filter(0, 0, 1, 0, 1000.0f, 1.0f, 1.0f));
|
||||
RequireRejected<std::runtime_error>(sampler, Filter(0, 1, 2, 0, 1000.0f, 1.0f, 1.0f));
|
||||
RequireRejected<std::invalid_argument>(sampler, Filter(0, 1, 1, 0, NAN, 1.0f, 1.0f));
|
||||
RequireRejected<std::invalid_argument>(sampler, Filter(0, 1, 1, 0, -1.0f, 1.0f, 1.0f));
|
||||
RequireRejected<std::invalid_argument>(sampler, Filter(0, 1, 1, 0, 1000.0f, 0.0f, 1.0f));
|
||||
RequireRejected<std::invalid_argument>(sampler, Filter(0, 1, 1, 0, 1000.0f, 1.0f, -0.5f));
|
||||
auto small = Filter(0, 1, 1, 0, 1000.0f, 1.0f, 1.0f);
|
||||
small.header.size = sizeof(small) - 4;
|
||||
RequireRejected<std::invalid_argument>(sampler, small);
|
||||
Require(sceNgs2SystemDestroy(system, nullptr) == SCE_NGS2_OK);
|
||||
}
|
||||
|
||||
int main() {
|
||||
TestLowPassAndTail();
|
||||
TestBypassMaskAndGain();
|
||||
TestSystemRateAndZeroCutoff();
|
||||
TestRejectedParams();
|
||||
return 0;
|
||||
}
|
||||
|
|
@ -124,6 +124,7 @@ Throughout the project, every function at every stage either **does exactly what
|
|||
- [sceShareGetCurrentStatus](../../core/libs/prx/libSceShare/Export.cpp) (libSceShare) - validation (feature flag non-zero, status non-null) and the all-zero 16-byte status taken from the [KytyPS5](https://github.com/KytyPS5/KytyPS5) reimplementation, not confirmed on a PS5 title; the meaning of the recording status values is unknown
|
||||
- [sceAgcDcbSetZPassPredicationEnableGetSize, sceAgcDcbSetPredicationDisableGetSize, sceAgcDcbSetBoolPredicationEnableGetSize](../../core/libs/prx/libSceAgc/DcbState/src/Predication.cpp) (libSceAgc) - the builders are inline in the SDK and not exported; the size is assumed to be that of the SET_PREDICATION packet `sceAgcDcbSetPredication` writes (16 bytes)
|
||||
- [Resource registration](../../core/libs/prx/libSceAgcDriver/Resource/src/Registration.cpp) (libSceAgcDriver) - registration always fails with `RESOURCE_REGISTRATION_UNAVAILABLE`, so the owner and resource queries, `sceAgcDriverSetResourceUserData`, `sceAgcDriverFindResourcesPublic` and `sceAgcDriverUnregisterAllResourcesForOwner` return the same error without reading their arguments. Their parameter lists follow the libSceGnmDriver counterparts and are unverified
|
||||
- [sceNgs2VoiceControl sampler filter](../../core/libs/prx/libSceNgs2.native/src/Voice.cpp) (libSceNgs2.native) - param `0x1000000a`, its layout and meaning follow KytyPS5 only: location 1 type 1 is an RBJ low-pass biquad at the system sample rate after resampling, scaled by `level`, a set `channel_mask` bit bypasses that channel, type 0 turns the filter off. Other locations and types throw. The voice keeps playing a filter tail after its last block until the history is zero, with outputs below 1e-20 flushed to zero (a KytyPS5 choice); the console's tail length is unknown
|
||||
|
||||
### Functional
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue