diff --git a/src/audio_core/renderer/command/command_buffer.cpp b/src/audio_core/renderer/command/command_buffer.cpp index 5a009b381..b65d17d76 100644 --- a/src/audio_core/renderer/command/command_buffer.cpp +++ b/src/audio_core/renderer/command/command_buffer.cpp @@ -227,6 +227,133 @@ void CommandBuffer::GenerateBiquadFilterCommand(const s32 node_id, VoiceInfo& vo const s16 buffer_count, const s8 channel, const u32 biquad_index, 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)}; cmd.input = buffer_count + channel; @@ -301,20 +428,15 @@ void CommandBuffer::GenerateBiquadFilterCommand(const s32 node_id, EffectInfoBas 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)); @@ -649,10 +771,18 @@ void CommandBuffer::GenerateCopyMixBufferCommand(const s32 node_id, EffectInfoBa 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 ff3b6828d..235fba1d1 100644 --- a/src/audio_core/renderer/command/command_generator.cpp +++ b/src/audio_core/renderer/command/command_generator.cpp @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2022 yuzu 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,30 +363,83 @@ void CommandGenerator::GenerateAuxCommand(const s16 buffer_offset, EffectInfoBas void CommandGenerator::GenerateBiquadFilterEffectCommand(const s16 buffer_offset, EffectInfoBase& effect_info, const s32 node_id) { - + // Handle ParameterVersion2 (REV15+) if (render_context.behavior->IsEffectInfoVersion2Supported()) { - const auto& parameter_v2{ + const auto& param_v2{ *reinterpret_cast(effect_info.GetParameter())}; - const bool needs_init = false; + + // 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(); - const s8 channels = parameter_v2.channel_count > 0 ? parameter_v2.channel_count : 2; - if (effect_info.IsEnabled()) { - for (s8 channel = 0; channel < channels; channel++) { - command_buffer.GenerateBiquadFilterCommand( - node_id, effect_info, buffer_offset, channel, needs_init, use_float_processing); - } - } else { - for (s8 channel = 0; channel < channels; channel++) { - command_buffer.GenerateCopyMixBufferCommand(node_id, effect_info, buffer_offset, - channel); - } + + // 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; } - const auto& parameter{ + 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) { @@ -400,8 +455,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; } @@ -410,11 +467,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); - } } } diff --git a/src/audio_core/renderer/command/effect/biquad_filter.cpp b/src/audio_core/renderer/command/effect/biquad_filter.cpp index 1bc23012b..fdb0f5a7c 100644 --- a/src/audio_core/renderer/command/effect/biquad_filter.cpp +++ b/src/audio_core/renderer/command/effect/biquad_filter.cpp @@ -27,25 +27,28 @@ void ApplyBiquadFilterFloat(std::span output, std::span input, Common::FixedPoint<50, 14>::from_base(b_[2]).to_double()}; std::array a{Common::FixedPoint<50, 14>::from_base(a_[0]).to_double(), Common::FixedPoint<50, 14>::from_base(a_[1]).to_double()}; - std::array s{Common::BitCast(state.s0), Common::BitCast(state.s1), - Common::BitCast(state.s2), Common::BitCast(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(state.s0)}; + f64 s1{Common::BitCast(state.s1)}; for (u32 i = 0; i < sample_count; i++) { f64 in_sample{static_cast(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(std::clamp(sample, min, max)); - s[1] = s[0]; - s[0] = in_sample; - s[3] = s[2]; - s[2] = sample; + // Update state using Direct Form 2 + s0 = in_sample * b[1] + sample * a[0] + s1; + s1 = in_sample * b[2] + sample * a[1]; } - state.s0 = Common::BitCast(s[0]); - state.s1 = Common::BitCast(s[1]); - state.s2 = Common::BitCast(s[2]); - state.s3 = Common::BitCast(s[3]); + state.s0 = Common::BitCast(s0); + state.s1 = Common::BitCast(s1); + // s2 and s3 are unused in Direct Form 2, but we keep them zeroed for consistency + state.s2 = 0; + state.s3 = 0; } /** @@ -57,29 +60,30 @@ void ApplyBiquadFilterFloat2(std::span output, std::span input, constexpr f64 min{std::numeric_limits::min()}; constexpr f64 max{std::numeric_limits::max()}; - std::array b_double{static_cast(b[0]), static_cast(b[1]), - static_cast(b[2])}; + std::array b_double{static_cast(b[0]), static_cast(b[1]), static_cast(b[2])}; std::array a_double{static_cast(a[0]), static_cast(a[1])}; - std::array s{Common::BitCast(state.s0), Common::BitCast(state.s1), - Common::BitCast(state.s2), Common::BitCast(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(state.s0)}; + f64 s1{Common::BitCast(state.s1)}; for (u32 i = 0; i < sample_count; i++) { f64 in_sample{static_cast(input[i])}; - auto sample{in_sample * b_double[0] + s[0] * b_double[1] + s[1] * b_double[2] + - s[2] * a_double[0] + s[3] * a_double[1]}; + f64 sample{in_sample * b_double[0] + s0}; output[i] = static_cast(std::clamp(sample, min, max)); - s[1] = s[0]; - s[0] = in_sample; - s[3] = s[2]; - s[2] = sample; + // Update state using Direct Form 2 + s0 = in_sample * b_double[1] + sample * a_double[0] + s1; + s1 = in_sample * b_double[2] + sample * a_double[1]; } - state.s0 = Common::BitCast(s[0]); - state.s1 = Common::BitCast(s[1]); - state.s2 = Common::BitCast(s[2]); - state.s3 = Common::BitCast(s[3]); + state.s0 = Common::BitCast(s0); + state.s1 = Common::BitCast(s1); + // s2 and s3 are unused in Direct Form 2, but we keep them zeroed for consistency + state.s2 = 0; + state.s3 = 0; } /** @@ -118,21 +122,58 @@ void BiquadFilterCommand::Dump( } void BiquadFilterCommand::Process(const AudioRenderer::CommandListProcessor& processor) { + if (state == 0) { + LOG_ERROR(Service_Audio, "BiquadFilterCommand: Invalid state pointer (null)"); + return; + } + auto state_{reinterpret_cast(state)}; if (needs_init) { *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(input) * processor.sample_count; + const u64 output_offset = static_cast(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{ - processor.mix_buffers.subspan(input * processor.sample_count, processor.sample_count)}; + processor.mix_buffers.subspan(input_offset, processor.sample_count)}; 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) { // REV15+: Use native float coefficients if available if (use_float_coefficients) { ApplyBiquadFilterFloat2(output_buffer, input_buffer, biquad_float.numerator, - biquad_float.denominator, *state_, processor.sample_count); + biquad_float.denominator, *state_, processor.sample_count); } else { ApplyBiquadFilterFloat(output_buffer, input_buffer, biquad.b, biquad.a, *state_, processor.sample_count); @@ -144,6 +185,44 @@ void BiquadFilterCommand::Process(const AudioRenderer::CommandListProcessor& pro } 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(input) * processor.sample_count; + const u64 output_offset = static_cast(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; } diff --git a/src/audio_core/renderer/effect/biquad_filter.cpp b/src/audio_core/renderer/effect/biquad_filter.cpp index 993701786..edf58d2a6 100644 --- a/src/audio_core/renderer/effect/biquad_filter.cpp +++ b/src/audio_core/renderer/effect/biquad_filter.cpp @@ -1,6 +1,8 @@ // SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include + #include "audio_core/renderer/effect/biquad_filter.h" namespace AudioCore::Renderer { @@ -11,9 +13,44 @@ void BiquadFilterInfo::Update(BehaviorInfo::ErrorInfo& error_info, auto params{reinterpret_cast(parameter.data())}; std::memcpy(params, in_specific, sizeof(ParameterVersion1)); + // Check for corrupted parameters - if detected, disable the effect to prevent audio issues + bool parameters_valid = true; + + // Validate channel_count + if (params->channel_count < 0 || static_cast(params->channel_count) > MaxChannels) { + LOG_WARNING(Service_Audio, "BiquadFilterInfo: Invalid channel_count {}, disabling effect", + params->channel_count); + parameters_valid = false; + } + + // Validate parameter state + if (static_cast(params->state) > static_cast(EffectInfoBase::ParameterState::Updated)) { + LOG_WARNING(Service_Audio, "BiquadFilterInfo: Invalid parameter state {}, disabling effect", + static_cast(params->state)); + parameters_valid = false; + } + + // Validate input/output buffer indices + for (s8 i = 0; static_cast(i) < MaxChannels && parameters_valid; i++) { + // Negative values are allowed (indicate unused channels), but check for out-of-range values + if (params->inputs[i] < -1 || params->inputs[i] >= static_cast(MaxChannels * 2)) { + LOG_WARNING(Service_Audio, "BiquadFilterInfo: Invalid input buffer index {} for channel {}, disabling effect", + params->inputs[i], i); + parameters_valid = false; + break; + } + if (params->outputs[i] < -1 || params->outputs[i] >= static_cast(MaxChannels * 2)) { + LOG_WARNING(Service_Audio, "BiquadFilterInfo: Invalid output buffer index {} for channel {}, disabling effect", + params->outputs[i], i); + parameters_valid = false; + break; + } + } + mix_id = in_params.mix_id; process_order = in_params.process_order; - enabled = in_params.enabled; + // Disable effect if parameters are corrupted to prevent audio issues + enabled = in_params.enabled && parameters_valid; error_info.error_code = ResultSuccess; error_info.address = CpuAddr(0); @@ -25,9 +62,48 @@ void BiquadFilterInfo::Update(BehaviorInfo::ErrorInfo& error_info, auto params{reinterpret_cast(parameter.data())}; std::memcpy(params, in_specific, sizeof(ParameterVersion2)); + + // Check for corrupted parameters - if detected, disable the effect to prevent audio issues + bool parameters_valid = true; + + // Validate channel_count + if (params->channel_count < 0 || static_cast(params->channel_count) > MaxChannels) { + LOG_WARNING(Service_Audio, "BiquadFilterInfo: Invalid channel_count {}, disabling effect", + params->channel_count); + parameters_valid = false; + } + + // Validate input/output buffer indices only for active channels + const s8 active_channels = parameters_valid ? params->channel_count : 0; + for (s8 i = 0; static_cast(i) < MaxChannels && i < active_channels && parameters_valid; i++) { + // Negative values are allowed (indicate unused channels), but check for out-of-range values + if (params->inputs[i] < -1 || params->inputs[i] >= static_cast(MaxChannels * 2)) { + LOG_WARNING(Service_Audio, "BiquadFilterInfo: Invalid input buffer index {} for channel {}, disabling effect", + params->inputs[i], i); + parameters_valid = false; + break; + } + if (params->outputs[i] < -1 || params->outputs[i] >= static_cast(MaxChannels * 2)) { + LOG_WARNING(Service_Audio, "BiquadFilterInfo: Invalid output buffer index {} for channel {}, disabling effect", + params->outputs[i], i); + parameters_valid = false; + break; + } + } + + // Validate parameter state + if (static_cast(params->state) > static_cast(EffectInfoBase::ParameterState::Updated)) { + LOG_WARNING(Service_Audio, "BiquadFilterInfo: Invalid parameter state {}, disabling effect", + static_cast(params->state)); + parameters_valid = false; + } + mix_id = in_params.mix_id; process_order = in_params.process_order; - enabled = in_params.enabled; + // ParameterVersion2 uses state field similar to v1 + // Effect is enabled if in_params.enabled, parameters are valid, and state is within valid range + enabled = in_params.enabled && parameters_valid && + static_cast(params->state) <= static_cast(EffectInfoBase::ParameterState::Updated); error_info.error_code = ResultSuccess; error_info.address = CpuAddr(0); diff --git a/src/audio_core/sink/sink_stream.cpp b/src/audio_core/sink/sink_stream.cpp index 0a98eb31e..8e562f03b 100644 --- a/src/audio_core/sink/sink_stream.cpp +++ b/src/audio_core/sink/sink_stream.cpp @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later +#include #include #include #include @@ -229,10 +230,19 @@ void SinkStream::ProcessAudioOutAndRender(std::span output_buffer, std::siz // If the playing buffer has been consumed or has no frames, we need a new one if (playing_buffer.consumed || playing_buffer.frames == 0) { if (!queue.try_dequeue(playing_buffer)) { - // If no buffer was available we've underrun, fill the remaining buffer with - // the last written frame and continue. - for (size_t i = frames_written; i < num_frames; i++) { - std::memcpy(&output_buffer[i * frame_size], &last_frame[0], frame_size_bytes); + // If no buffer was available we've underrun, repeat the last frame to maintain + // audio continuity. Use the last valid frame we played to avoid harsh transitions. + if (frames_written > 0) { + // We have a valid last_frame, repeat it smoothly + for (size_t i = frames_written; i < num_frames; i++) { + std::memcpy(&output_buffer[i * frame_size], &last_frame[0], frame_size_bytes); + } + } else { + // No frames written yet, fill with silence + static constexpr std::array silence{}; + for (size_t i = frames_written; i < num_frames; i++) { + std::memcpy(&output_buffer[i * frame_size], &silence[0], frame_size_bytes); + } } frames_written = num_frames; continue;