diff --git a/src/audio_core/renderer/command/command_buffer.cpp b/src/audio_core/renderer/command/command_buffer.cpp index e338c1ae5..cd1ed3c92 100644 --- a/src/audio_core/renderer/command/command_buffer.cpp +++ b/src/audio_core/renderer/command/command_buffer.cpp @@ -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(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(param_v2.state) > static_cast(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(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(param_v2.b[0]) / q14_scale; + cmd.biquad_float.numerator[1] = static_cast(param_v2.b[1]) / q14_scale; + cmd.biquad_float.numerator[2] = static_cast(param_v2.b[2]) / q14_scale; + cmd.biquad_float.denominator[0] = static_cast(param_v2.a[0]) / q14_scale; + cmd.biquad_float.denominator[1] = static_cast(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( + 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(cmd); + return; + } + + // Handle ParameterVersion1 (legacy) + const auto& param_v1{ + *reinterpret_cast(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(node_id)}; - const auto& parameter{ - *reinterpret_cast(effect_info.GetParameter())}; - const auto state{reinterpret_cast( - 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( + 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(node_id)}; - const auto& parameter{ - *reinterpret_cast(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(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(effect_info.GetParameter())}; + cmd.input_index = buffer_offset + param_v1.inputs[channel]; + cmd.output_index = buffer_offset + param_v1.outputs[channel]; + } GenerateEnd(cmd); } diff --git a/src/audio_core/renderer/command/command_generator.cpp b/src/audio_core/renderer/command/command_generator.cpp index f97db5899..1a0914fcb 100644 --- a/src/audio_core/renderer/command/command_generator.cpp +++ b/src/audio_core/renderer/command/command_generator.cpp @@ -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 + #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(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(channel_count) > MaxChannels) { + LOG_WARNING(Service_Audio, "BiquadFilterEffectCommand: Invalid v2 channel_count {}, skipping", + channel_count); + return; + } + + // Validate parameter state + if (static_cast(param_v2.state) > static_cast(EffectInfoBase::ParameterState::Updated)) { + LOG_WARNING(Service_Audio, "BiquadFilterEffectCommand: Invalid v2 parameter state {}, skipping", + static_cast(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(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(parameter.channel_count) > MaxChannels) { + LOG_WARNING(Service_Audio, "BiquadFilterEffectCommand: Invalid channel_count {}, skipping command generation", + parameter.channel_count); + return; + } + + if (static_cast(parameter.state) > static_cast(EffectInfoBase::ParameterState::Updated)) { + LOG_WARNING(Service_Audio, "BiquadFilterEffectCommand: Invalid parameter state {}, skipping command generation", + static_cast(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(parameter.state)); + // Should not reach here after validation, but handle gracefully + LOG_WARNING(Service_Audio, "BiquadFilterEffectCommand: Unexpected state {}, treating as Updated", + static_cast(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); - } } }