diff --git a/src/audio_core/renderer/command/effect/biquad_filter.cpp b/src/audio_core/renderer/command/effect/biquad_filter.cpp index d40fa71b7..e900a0e28 100644 --- a/src/audio_core/renderer/command/effect/biquad_filter.cpp +++ b/src/audio_core/renderer/command/effect/biquad_filter.cpp @@ -8,7 +8,8 @@ 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 input - Input container for samples to be filtered. @@ -27,29 +28,33 @@ 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; } /** * Biquad filter float implementation with native float coefficients (SDK REV15+). + * Uses Direct Form 2 for better numerical stability, matching Ryujinx. */ void ApplyBiquadFilterFloat2(std::span output, std::span input, std::array& b, std::array& a, @@ -59,26 +64,28 @@ void ApplyBiquadFilterFloat2(std::span output, std::span input, 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; } /** @@ -117,15 +124,52 @@ 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 @@ -143,6 +187,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; }