mirror of
https://git.citron-emu.org/citron/emulator
synced 2025-12-24 04:33:44 +00:00
fix(audio): Add biquad filter v2 validation and coefficient conversion
- Add buffer index validation to prevent ArgumentOutOfRange errors - Convert fixed-point coefficients (Q14) to float for REV15+ processing - Add state-based initialization logic for ParameterVersion2 - Validate raw buffer indices before adding buffer_offset - Add proper state handling in command generation Signed-off-by: Zephyron <zephyron@citron-emu.org>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "audio_core/renderer/behavior/behavior_info.h"
|
||||
@@ -255,22 +256,144 @@ void CommandBuffer::GenerateBiquadFilterCommand(const s32 node_id, EffectInfoBas
|
||||
const s16 buffer_offset, const s8 channel,
|
||||
const bool needs_init,
|
||||
const bool use_float_processing) {
|
||||
const bool is_v2 = behavior && behavior->IsEffectInfoVersion2Supported();
|
||||
|
||||
// Handle ParameterVersion2 (REV15+)
|
||||
if (is_v2) {
|
||||
const auto& param_v2{
|
||||
*reinterpret_cast<BiquadFilterInfo::ParameterVersion2*>(effect_info.GetParameter())};
|
||||
|
||||
// Validate channel bounds
|
||||
if (channel < 0 || channel >= param_v2.channel_count) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check state field - if state is out of valid range, generate copy command instead
|
||||
if (static_cast<u8>(param_v2.state) > static_cast<u8>(EffectInfoBase::ParameterState::Updated)) {
|
||||
GenerateCopyMixBufferCommand(node_id, effect_info, buffer_offset, channel);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate raw buffer indices before adding offset (similar to Ryujinx's ArgumentOutOfRange check)
|
||||
const s8 raw_input = param_v2.inputs[channel];
|
||||
const s8 raw_output = param_v2.outputs[channel];
|
||||
|
||||
// Validate raw indices are within reasonable bounds (negative values are allowed for unused channels)
|
||||
// Maximum reasonable buffer index is typically 24-32, use 64 as a safe upper bound
|
||||
constexpr s8 MaxReasonableBufferIndex = 64;
|
||||
if (raw_input < -1 || raw_input >= MaxReasonableBufferIndex) {
|
||||
LOG_WARNING(Service_Audio,
|
||||
"BiquadFilterCommand: Skipping command generation - raw input index out of range ({})",
|
||||
raw_input);
|
||||
return;
|
||||
}
|
||||
if (raw_output < -1 || raw_output >= MaxReasonableBufferIndex) {
|
||||
LOG_WARNING(Service_Audio,
|
||||
"BiquadFilterCommand: Skipping command generation - raw output index out of range ({})",
|
||||
raw_output);
|
||||
return;
|
||||
}
|
||||
|
||||
const s16 input_index = buffer_offset + raw_input;
|
||||
const s16 output_index = buffer_offset + raw_output;
|
||||
|
||||
// Validate final buffer indices
|
||||
if (input_index < 0) {
|
||||
LOG_WARNING(Service_Audio,
|
||||
"BiquadFilterCommand: Skipping command generation - invalid input index ({})",
|
||||
input_index);
|
||||
return;
|
||||
}
|
||||
|
||||
const s16 effective_output = (output_index < 0) ? input_index : output_index;
|
||||
if (output_index < 0) {
|
||||
LOG_WARNING(Service_Audio,
|
||||
"BiquadFilterCommand: Invalid output index ({}), using input ({}) for in-place "
|
||||
"processing",
|
||||
output_index, input_index);
|
||||
}
|
||||
|
||||
auto& cmd{GenerateStart<BiquadFilterCommand, CommandId::BiquadFilter>(node_id)};
|
||||
|
||||
cmd.input = input_index;
|
||||
cmd.output = effective_output;
|
||||
|
||||
// Convert fixed-point coefficients (Q14 format) to float for REV15+ float processing
|
||||
// Q14 means 14 fractional bits, so divide by 2^14 = 16384.0f
|
||||
constexpr f32 q14_scale = 16384.0f;
|
||||
cmd.biquad_float.numerator[0] = static_cast<f32>(param_v2.b[0]) / q14_scale;
|
||||
cmd.biquad_float.numerator[1] = static_cast<f32>(param_v2.b[1]) / q14_scale;
|
||||
cmd.biquad_float.numerator[2] = static_cast<f32>(param_v2.b[2]) / q14_scale;
|
||||
cmd.biquad_float.denominator[0] = static_cast<f32>(param_v2.a[0]) / q14_scale;
|
||||
cmd.biquad_float.denominator[1] = static_cast<f32>(param_v2.a[1]) / q14_scale;
|
||||
cmd.use_float_coefficients = true;
|
||||
|
||||
// Translate state buffer - state pointer is already per-channel, so translate one state only
|
||||
const auto state{reinterpret_cast<VoiceState::BiquadFilterState*>(
|
||||
effect_info.GetStateBuffer() + channel * sizeof(VoiceState::BiquadFilterState))};
|
||||
cmd.state = memory_pool->Translate(CpuAddr(state), sizeof(VoiceState::BiquadFilterState));
|
||||
|
||||
cmd.needs_init = needs_init;
|
||||
cmd.use_float_processing = use_float_processing;
|
||||
|
||||
GenerateEnd<BiquadFilterCommand>(cmd);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle ParameterVersion1 (legacy)
|
||||
const auto& param_v1{
|
||||
*reinterpret_cast<BiquadFilterInfo::ParameterVersion1*>(effect_info.GetParameter())};
|
||||
|
||||
// Validate raw buffer indices before adding offset (similar to Ryujinx's ArgumentOutOfRange check)
|
||||
const s8 raw_input = param_v1.inputs[channel];
|
||||
const s8 raw_output = param_v1.outputs[channel];
|
||||
|
||||
// Validate raw indices are within reasonable bounds (negative values are allowed for unused channels)
|
||||
// Maximum reasonable buffer index is typically 24-32, use 64 as a safe upper bound
|
||||
constexpr s8 MaxReasonableBufferIndex = 64;
|
||||
if (raw_input < -1 || raw_input >= MaxReasonableBufferIndex) {
|
||||
LOG_WARNING(Service_Audio,
|
||||
"BiquadFilterCommand: Skipping command generation - raw input index out of range ({})",
|
||||
raw_input);
|
||||
return;
|
||||
}
|
||||
if (raw_output < -1 || raw_output >= MaxReasonableBufferIndex) {
|
||||
LOG_WARNING(Service_Audio,
|
||||
"BiquadFilterCommand: Skipping command generation - raw output index out of range ({})",
|
||||
raw_output);
|
||||
return;
|
||||
}
|
||||
|
||||
const s16 input_index = buffer_offset + raw_input;
|
||||
const s16 output_index = buffer_offset + raw_output;
|
||||
|
||||
// Validate and correct buffer indices
|
||||
if (input_index < 0) {
|
||||
LOG_WARNING(Service_Audio,
|
||||
"BiquadFilterCommand: Skipping command generation - invalid input index ({})",
|
||||
input_index);
|
||||
return;
|
||||
}
|
||||
|
||||
const s16 effective_output = (output_index < 0) ? input_index : output_index;
|
||||
if (output_index < 0) {
|
||||
LOG_WARNING(Service_Audio,
|
||||
"BiquadFilterCommand: Invalid output index ({}), using input ({}) for in-place "
|
||||
"processing",
|
||||
output_index, input_index);
|
||||
}
|
||||
|
||||
auto& cmd{GenerateStart<BiquadFilterCommand, CommandId::BiquadFilter>(node_id)};
|
||||
|
||||
const auto& parameter{
|
||||
*reinterpret_cast<BiquadFilterInfo::ParameterVersion1*>(effect_info.GetParameter())};
|
||||
const auto state{reinterpret_cast<VoiceState::BiquadFilterState*>(
|
||||
effect_info.GetStateBuffer() + channel * sizeof(VoiceState::BiquadFilterState))};
|
||||
|
||||
cmd.input = buffer_offset + parameter.inputs[channel];
|
||||
cmd.output = buffer_offset + parameter.outputs[channel];
|
||||
|
||||
cmd.biquad.b = parameter.b;
|
||||
cmd.biquad.a = parameter.a;
|
||||
|
||||
// Effects use legacy fixed-point format
|
||||
cmd.input = input_index;
|
||||
cmd.output = effective_output;
|
||||
cmd.biquad.b = param_v1.b;
|
||||
cmd.biquad.a = param_v1.a;
|
||||
cmd.use_float_coefficients = false;
|
||||
|
||||
// Translate state buffer address for DSP (v1 uses full buffer size)
|
||||
const auto state{reinterpret_cast<VoiceState::BiquadFilterState*>(
|
||||
effect_info.GetStateBuffer() + channel * sizeof(VoiceState::BiquadFilterState))};
|
||||
cmd.state = memory_pool->Translate(CpuAddr(state),
|
||||
MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState));
|
||||
|
||||
@@ -593,10 +716,18 @@ void CommandBuffer::GenerateCopyMixBufferCommand(const s32 node_id, EffectInfoBa
|
||||
const s16 buffer_offset, const s8 channel) {
|
||||
auto& cmd{GenerateStart<CopyMixBufferCommand, CommandId::CopyMixBuffer>(node_id)};
|
||||
|
||||
const auto& parameter{
|
||||
*reinterpret_cast<BiquadFilterInfo::ParameterVersion1*>(effect_info.GetParameter())};
|
||||
cmd.input_index = buffer_offset + parameter.inputs[channel];
|
||||
cmd.output_index = buffer_offset + parameter.outputs[channel];
|
||||
// Extract buffer indices based on parameter version
|
||||
if (behavior && behavior->IsEffectInfoVersion2Supported()) {
|
||||
const auto& param_v2{
|
||||
*reinterpret_cast<BiquadFilterInfo::ParameterVersion2*>(effect_info.GetParameter())};
|
||||
cmd.input_index = buffer_offset + param_v2.inputs[channel];
|
||||
cmd.output_index = buffer_offset + param_v2.outputs[channel];
|
||||
} else {
|
||||
const auto& param_v1{
|
||||
*reinterpret_cast<BiquadFilterInfo::ParameterVersion1*>(effect_info.GetParameter())};
|
||||
cmd.input_index = buffer_offset + param_v1.inputs[channel];
|
||||
cmd.output_index = buffer_offset + param_v1.outputs[channel];
|
||||
}
|
||||
|
||||
GenerateEnd<CopyMixBufferCommand>(cmd);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "audio_core/common/audio_renderer_parameter.h"
|
||||
#include "audio_core/renderer/behavior/behavior_info.h"
|
||||
#include "audio_core/renderer/command/command_buffer.h"
|
||||
@@ -361,9 +364,83 @@ void CommandGenerator::GenerateAuxCommand(const s16 buffer_offset, EffectInfoBas
|
||||
void CommandGenerator::GenerateBiquadFilterEffectCommand(const s16 buffer_offset,
|
||||
EffectInfoBase& effect_info,
|
||||
const s32 node_id) {
|
||||
const auto& parameter{
|
||||
// Handle ParameterVersion2 (REV15+)
|
||||
if (render_context.behavior->IsEffectInfoVersion2Supported()) {
|
||||
const auto& param_v2{
|
||||
*reinterpret_cast<BiquadFilterInfo::ParameterVersion2*>(effect_info.GetParameter())};
|
||||
|
||||
// Early return if effect is disabled or parameters are invalid
|
||||
if (!effect_info.IsEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate channel count to prevent out-of-bounds access
|
||||
const s8 channel_count = param_v2.channel_count;
|
||||
if (channel_count < 0 || static_cast<u32>(channel_count) > MaxChannels) {
|
||||
LOG_WARNING(Service_Audio, "BiquadFilterEffectCommand: Invalid v2 channel_count {}, skipping",
|
||||
channel_count);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate parameter state
|
||||
if (static_cast<u8>(param_v2.state) > static_cast<u8>(EffectInfoBase::ParameterState::Updated)) {
|
||||
LOG_WARNING(Service_Audio, "BiquadFilterEffectCommand: Invalid v2 parameter state {}, skipping",
|
||||
static_cast<u8>(param_v2.state));
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine if initialization is needed based on state (similar to v1)
|
||||
bool needs_init = false;
|
||||
switch (param_v2.state) {
|
||||
case EffectInfoBase::ParameterState::Initialized:
|
||||
needs_init = true;
|
||||
break;
|
||||
case EffectInfoBase::ParameterState::Updating:
|
||||
case EffectInfoBase::ParameterState::Updated:
|
||||
if (render_context.behavior->IsBiquadFilterEffectStateClearBugFixed()) {
|
||||
needs_init = false;
|
||||
} else {
|
||||
needs_init = param_v2.state == EffectInfoBase::ParameterState::Updating;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
needs_init = false;
|
||||
break;
|
||||
}
|
||||
|
||||
const bool use_float_processing = render_context.behavior->UseBiquadFilterFloatProcessing();
|
||||
|
||||
// Generate commands for each active channel
|
||||
for (s8 channel = 0; channel < channel_count; channel++) {
|
||||
command_buffer.GenerateBiquadFilterCommand(
|
||||
node_id, effect_info, buffer_offset, channel, needs_init, use_float_processing);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
auto& parameter{
|
||||
*reinterpret_cast<BiquadFilterInfo::ParameterVersion1*>(effect_info.GetParameter())};
|
||||
if (effect_info.IsEnabled()) {
|
||||
|
||||
// If effect is disabled (e.g., due to corrupted parameters), skip command generation
|
||||
if (!effect_info.IsEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate parameters - if corrupted, skip command generation to prevent audio issues
|
||||
if (parameter.channel_count < 0 || static_cast<u32>(parameter.channel_count) > MaxChannels) {
|
||||
LOG_WARNING(Service_Audio, "BiquadFilterEffectCommand: Invalid channel_count {}, skipping command generation",
|
||||
parameter.channel_count);
|
||||
return;
|
||||
}
|
||||
|
||||
if (static_cast<u8>(parameter.state) > static_cast<u8>(EffectInfoBase::ParameterState::Updated)) {
|
||||
LOG_WARNING(Service_Audio, "BiquadFilterEffectCommand: Invalid parameter state {}, skipping command generation",
|
||||
static_cast<u8>(parameter.state));
|
||||
return;
|
||||
}
|
||||
|
||||
// Effect is enabled and parameters are valid - generate commands
|
||||
{
|
||||
bool needs_init{false};
|
||||
|
||||
switch (parameter.state) {
|
||||
@@ -379,8 +456,10 @@ void CommandGenerator::GenerateBiquadFilterEffectCommand(const s16 buffer_offset
|
||||
}
|
||||
break;
|
||||
default:
|
||||
LOG_ERROR(Service_Audio, "Invalid biquad parameter state {}",
|
||||
static_cast<u32>(parameter.state));
|
||||
// Should not reach here after validation, but handle gracefully
|
||||
LOG_WARNING(Service_Audio, "BiquadFilterEffectCommand: Unexpected state {}, treating as Updated",
|
||||
static_cast<u8>(parameter.state));
|
||||
needs_init = false;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -389,11 +468,6 @@ void CommandGenerator::GenerateBiquadFilterEffectCommand(const s16 buffer_offset
|
||||
node_id, effect_info, buffer_offset, channel, needs_init,
|
||||
render_context.behavior->UseBiquadFilterFloatProcessing());
|
||||
}
|
||||
} else {
|
||||
for (s8 channel = 0; channel < parameter.channel_count; channel++) {
|
||||
command_buffer.GenerateCopyMixBufferCommand(node_id, effect_info, buffer_offset,
|
||||
channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user