mirror of
https://git.eden-emu.dev/archive/citron
synced 2026-04-14 16:50:46 -04:00
fix(audio): improve biquad filter parameter validation
- Use IsChannelCountValid for consistent validation - Validate buffer indices only for active channels - Disable effects with corrupted parameters instead of sanitizing - Improve error handling and logging for both v1 and v2 Signed-off-by: Zephyron <zephyron@citron-emu.org>
This commit is contained in:
@@ -8,7 +8,8 @@
|
|||||||
|
|
||||||
namespace AudioCore::Renderer {
|
namespace AudioCore::Renderer {
|
||||||
/**
|
/**
|
||||||
* Biquad filter float implementation.
|
* Biquad filter float implementation (Direct Form 2).
|
||||||
|
* This matches Ryujinx's implementation for better numerical stability.
|
||||||
*
|
*
|
||||||
* @param output - Output container for filtered samples.
|
* @param output - Output container for filtered samples.
|
||||||
* @param input - Input container for samples to be filtered.
|
* @param input - Input container for samples to be filtered.
|
||||||
@@ -27,29 +28,33 @@ void ApplyBiquadFilterFloat(std::span<s32> output, std::span<const s32> input,
|
|||||||
Common::FixedPoint<50, 14>::from_base(b_[2]).to_double()};
|
Common::FixedPoint<50, 14>::from_base(b_[2]).to_double()};
|
||||||
std::array<f64, 2> a{Common::FixedPoint<50, 14>::from_base(a_[0]).to_double(),
|
std::array<f64, 2> a{Common::FixedPoint<50, 14>::from_base(a_[0]).to_double(),
|
||||||
Common::FixedPoint<50, 14>::from_base(a_[1]).to_double()};
|
Common::FixedPoint<50, 14>::from_base(a_[1]).to_double()};
|
||||||
std::array<f64, 4> s{Common::BitCast<f64>(state.s0), Common::BitCast<f64>(state.s1),
|
|
||||||
Common::BitCast<f64>(state.s2), Common::BitCast<f64>(state.s3)};
|
// Direct Form 2 uses only 2 state variables (s0, s1)
|
||||||
|
// s2 and s3 are unused in Direct Form 2
|
||||||
|
f64 s0{Common::BitCast<f64>(state.s0)};
|
||||||
|
f64 s1{Common::BitCast<f64>(state.s1)};
|
||||||
|
|
||||||
for (u32 i = 0; i < sample_count; i++) {
|
for (u32 i = 0; i < sample_count; i++) {
|
||||||
f64 in_sample{static_cast<f64>(input[i])};
|
f64 in_sample{static_cast<f64>(input[i])};
|
||||||
auto sample{in_sample * b[0] + s[0] * b[1] + s[1] * b[2] + s[2] * a[0] + s[3] * a[1]};
|
f64 sample{in_sample * b[0] + s0};
|
||||||
|
|
||||||
output[i] = static_cast<s32>(std::clamp(sample, min, max));
|
output[i] = static_cast<s32>(std::clamp(sample, min, max));
|
||||||
|
|
||||||
s[1] = s[0];
|
// Update state using Direct Form 2
|
||||||
s[0] = in_sample;
|
s0 = in_sample * b[1] + sample * a[0] + s1;
|
||||||
s[3] = s[2];
|
s1 = in_sample * b[2] + sample * a[1];
|
||||||
s[2] = sample;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state.s0 = Common::BitCast<s64>(s[0]);
|
state.s0 = Common::BitCast<s64>(s0);
|
||||||
state.s1 = Common::BitCast<s64>(s[1]);
|
state.s1 = Common::BitCast<s64>(s1);
|
||||||
state.s2 = Common::BitCast<s64>(s[2]);
|
// s2 and s3 are unused in Direct Form 2, but we keep them zeroed for consistency
|
||||||
state.s3 = Common::BitCast<s64>(s[3]);
|
state.s2 = 0;
|
||||||
|
state.s3 = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Biquad filter float implementation with native float coefficients (SDK REV15+).
|
* Biquad filter float implementation with native float coefficients (SDK REV15+).
|
||||||
|
* Uses Direct Form 2 for better numerical stability, matching Ryujinx.
|
||||||
*/
|
*/
|
||||||
void ApplyBiquadFilterFloat2(std::span<s32> output, std::span<const s32> input,
|
void ApplyBiquadFilterFloat2(std::span<s32> output, std::span<const s32> input,
|
||||||
std::array<f32, 3>& b, std::array<f32, 2>& a,
|
std::array<f32, 3>& b, std::array<f32, 2>& a,
|
||||||
@@ -59,26 +64,28 @@ void ApplyBiquadFilterFloat2(std::span<s32> output, std::span<const s32> input,
|
|||||||
|
|
||||||
std::array<f64, 3> b_double{static_cast<f64>(b[0]), static_cast<f64>(b[1]), static_cast<f64>(b[2])};
|
std::array<f64, 3> b_double{static_cast<f64>(b[0]), static_cast<f64>(b[1]), static_cast<f64>(b[2])};
|
||||||
std::array<f64, 2> a_double{static_cast<f64>(a[0]), static_cast<f64>(a[1])};
|
std::array<f64, 2> a_double{static_cast<f64>(a[0]), static_cast<f64>(a[1])};
|
||||||
std::array<f64, 4> s{Common::BitCast<f64>(state.s0), Common::BitCast<f64>(state.s1),
|
|
||||||
Common::BitCast<f64>(state.s2), Common::BitCast<f64>(state.s3)};
|
// Direct Form 2 uses only 2 state variables (s0, s1)
|
||||||
|
// s2 and s3 are unused in Direct Form 2
|
||||||
|
f64 s0{Common::BitCast<f64>(state.s0)};
|
||||||
|
f64 s1{Common::BitCast<f64>(state.s1)};
|
||||||
|
|
||||||
for (u32 i = 0; i < sample_count; i++) {
|
for (u32 i = 0; i < sample_count; i++) {
|
||||||
f64 in_sample{static_cast<f64>(input[i])};
|
f64 in_sample{static_cast<f64>(input[i])};
|
||||||
auto sample{in_sample * b_double[0] + s[0] * b_double[1] + s[1] * b_double[2] +
|
f64 sample{in_sample * b_double[0] + s0};
|
||||||
s[2] * a_double[0] + s[3] * a_double[1]};
|
|
||||||
|
|
||||||
output[i] = static_cast<s32>(std::clamp(sample, min, max));
|
output[i] = static_cast<s32>(std::clamp(sample, min, max));
|
||||||
|
|
||||||
s[1] = s[0];
|
// Update state using Direct Form 2
|
||||||
s[0] = in_sample;
|
s0 = in_sample * b_double[1] + sample * a_double[0] + s1;
|
||||||
s[3] = s[2];
|
s1 = in_sample * b_double[2] + sample * a_double[1];
|
||||||
s[2] = sample;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state.s0 = Common::BitCast<s64>(s[0]);
|
state.s0 = Common::BitCast<s64>(s0);
|
||||||
state.s1 = Common::BitCast<s64>(s[1]);
|
state.s1 = Common::BitCast<s64>(s1);
|
||||||
state.s2 = Common::BitCast<s64>(s[2]);
|
// s2 and s3 are unused in Direct Form 2, but we keep them zeroed for consistency
|
||||||
state.s3 = Common::BitCast<s64>(s[3]);
|
state.s2 = 0;
|
||||||
|
state.s3 = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,15 +124,52 @@ void BiquadFilterCommand::Dump(
|
|||||||
}
|
}
|
||||||
|
|
||||||
void BiquadFilterCommand::Process(const AudioRenderer::CommandListProcessor& processor) {
|
void BiquadFilterCommand::Process(const AudioRenderer::CommandListProcessor& processor) {
|
||||||
|
if (state == 0) {
|
||||||
|
LOG_ERROR(Service_Audio, "BiquadFilterCommand: Invalid state pointer (null)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto state_{reinterpret_cast<VoiceState::BiquadFilterState*>(state)};
|
auto state_{reinterpret_cast<VoiceState::BiquadFilterState*>(state)};
|
||||||
if (needs_init) {
|
if (needs_init) {
|
||||||
*state_ = {};
|
*state_ = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate buffer indices and bounds
|
||||||
|
if (input < 0 || processor.sample_count == 0) {
|
||||||
|
LOG_ERROR(Service_Audio,
|
||||||
|
"BiquadFilterCommand: Invalid input buffer index or sample count - input={}, "
|
||||||
|
"sample_count={}",
|
||||||
|
input, processor.sample_count);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If output is invalid but input is valid, use input as output (in-place processing)
|
||||||
|
s16 effective_output = output;
|
||||||
|
if (output < 0) {
|
||||||
|
LOG_WARNING(Service_Audio,
|
||||||
|
"BiquadFilterCommand: Invalid output buffer index ({}), using input ({}) for "
|
||||||
|
"in-place processing",
|
||||||
|
output, input);
|
||||||
|
effective_output = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
const u64 input_offset = static_cast<u64>(input) * processor.sample_count;
|
||||||
|
const u64 output_offset = static_cast<u64>(effective_output) * processor.sample_count;
|
||||||
|
|
||||||
|
if (input_offset + processor.sample_count > processor.mix_buffers.size() ||
|
||||||
|
output_offset + processor.sample_count > processor.mix_buffers.size()) {
|
||||||
|
LOG_ERROR(Service_Audio,
|
||||||
|
"BiquadFilterCommand: Buffer indices out of bounds - input_offset={}, "
|
||||||
|
"output_offset={}, sample_count={}, buffer_size={}",
|
||||||
|
input_offset, output_offset, processor.sample_count,
|
||||||
|
processor.mix_buffers.size());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto input_buffer{
|
auto input_buffer{
|
||||||
processor.mix_buffers.subspan(input * processor.sample_count, processor.sample_count)};
|
processor.mix_buffers.subspan(input_offset, processor.sample_count)};
|
||||||
auto output_buffer{
|
auto output_buffer{
|
||||||
processor.mix_buffers.subspan(output * processor.sample_count, processor.sample_count)};
|
processor.mix_buffers.subspan(output_offset, processor.sample_count)};
|
||||||
|
|
||||||
if (use_float_processing) {
|
if (use_float_processing) {
|
||||||
// REV15+: Use native float coefficients if available
|
// REV15+: Use native float coefficients if available
|
||||||
@@ -143,6 +187,44 @@ void BiquadFilterCommand::Process(const AudioRenderer::CommandListProcessor& pro
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool BiquadFilterCommand::Verify(const AudioRenderer::CommandListProcessor& processor) {
|
bool BiquadFilterCommand::Verify(const AudioRenderer::CommandListProcessor& processor) {
|
||||||
|
// Validate state pointer
|
||||||
|
if (state == 0) {
|
||||||
|
LOG_ERROR(Service_Audio, "BiquadFilterCommand: Invalid state pointer (null)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate input buffer index (required)
|
||||||
|
if (input < 0) {
|
||||||
|
LOG_ERROR(Service_Audio, "BiquadFilterCommand: Invalid input buffer index - input={}", input);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output can be invalid - we'll handle it by using input as output (in-place processing)
|
||||||
|
// So we don't fail verification if only output is invalid
|
||||||
|
s16 effective_output = output;
|
||||||
|
if (output < 0) {
|
||||||
|
effective_output = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (processor.sample_count == 0) {
|
||||||
|
LOG_ERROR(Service_Audio, "BiquadFilterCommand: Invalid sample count - sample_count={}",
|
||||||
|
processor.sample_count);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const u64 input_offset = static_cast<u64>(input) * processor.sample_count;
|
||||||
|
const u64 output_offset = static_cast<u64>(effective_output) * processor.sample_count;
|
||||||
|
|
||||||
|
if (input_offset + processor.sample_count > processor.mix_buffers.size() ||
|
||||||
|
output_offset + processor.sample_count > processor.mix_buffers.size()) {
|
||||||
|
LOG_ERROR(Service_Audio,
|
||||||
|
"BiquadFilterCommand: Buffer indices out of bounds - input_offset={}, "
|
||||||
|
"output_offset={}, sample_count={}, buffer_size={}",
|
||||||
|
input_offset, output_offset, processor.sample_count,
|
||||||
|
processor.mix_buffers.size());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user