[Audio]: Improve BiquadFilter
This commit is contained in:
parent
3abe3b2d32
commit
63479f4de5
5 changed files with 420 additions and 73 deletions
|
|
@ -227,6 +227,133 @@ void CommandBuffer::GenerateBiquadFilterCommand(const s32 node_id, VoiceInfo& vo
|
||||||
const s16 buffer_count, const s8 channel,
|
const s16 buffer_count, const s8 channel,
|
||||||
const u32 biquad_index,
|
const u32 biquad_index,
|
||||||
const bool use_float_processing) {
|
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)};
|
auto& cmd{GenerateStart<BiquadFilterCommand, CommandId::BiquadFilter>(node_id)};
|
||||||
|
|
||||||
cmd.input = buffer_count + channel;
|
cmd.input = buffer_count + channel;
|
||||||
|
|
@ -301,20 +428,15 @@ void CommandBuffer::GenerateBiquadFilterCommand(const s32 node_id, EffectInfoBas
|
||||||
|
|
||||||
auto& cmd{GenerateStart<BiquadFilterCommand, CommandId::BiquadFilter>(node_id)};
|
auto& cmd{GenerateStart<BiquadFilterCommand, CommandId::BiquadFilter>(node_id)};
|
||||||
|
|
||||||
const auto& parameter{
|
cmd.input = input_index;
|
||||||
*reinterpret_cast<BiquadFilterInfo::ParameterVersion1*>(effect_info.GetParameter())};
|
cmd.output = effective_output;
|
||||||
const auto state{reinterpret_cast<VoiceState::BiquadFilterState*>(
|
cmd.biquad.b = param_v1.b;
|
||||||
effect_info.GetStateBuffer() + channel * sizeof(VoiceState::BiquadFilterState))};
|
cmd.biquad.a = param_v1.a;
|
||||||
|
|
||||||
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.use_float_coefficients = false;
|
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),
|
cmd.state = memory_pool->Translate(CpuAddr(state),
|
||||||
MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState));
|
MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState));
|
||||||
|
|
||||||
|
|
@ -649,10 +771,18 @@ void CommandBuffer::GenerateCopyMixBufferCommand(const s32 node_id, EffectInfoBa
|
||||||
|
|
||||||
auto& cmd{GenerateStart<CopyMixBufferCommand, CommandId::CopyMixBuffer>(node_id)};
|
auto& cmd{GenerateStart<CopyMixBufferCommand, CommandId::CopyMixBuffer>(node_id)};
|
||||||
|
|
||||||
const auto& parameter{
|
// 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())};
|
*reinterpret_cast<BiquadFilterInfo::ParameterVersion1*>(effect_info.GetParameter())};
|
||||||
cmd.input_index = buffer_offset + parameter.inputs[channel];
|
cmd.input_index = buffer_offset + param_v1.inputs[channel];
|
||||||
cmd.output_index = buffer_offset + parameter.outputs[channel];
|
cmd.output_index = buffer_offset + param_v1.outputs[channel];
|
||||||
|
}
|
||||||
|
|
||||||
GenerateEnd<CopyMixBufferCommand>(cmd);
|
GenerateEnd<CopyMixBufferCommand>(cmd);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include "audio_core/common/audio_renderer_parameter.h"
|
#include "audio_core/common/audio_renderer_parameter.h"
|
||||||
#include "audio_core/renderer/behavior/behavior_info.h"
|
#include "audio_core/renderer/behavior/behavior_info.h"
|
||||||
#include "audio_core/renderer/command/command_buffer.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,
|
void CommandGenerator::GenerateBiquadFilterEffectCommand(const s16 buffer_offset,
|
||||||
EffectInfoBase& effect_info,
|
EffectInfoBase& effect_info,
|
||||||
const s32 node_id) {
|
const s32 node_id) {
|
||||||
|
// Handle ParameterVersion2 (REV15+)
|
||||||
if (render_context.behavior->IsEffectInfoVersion2Supported()) {
|
if (render_context.behavior->IsEffectInfoVersion2Supported()) {
|
||||||
const auto& parameter_v2{
|
const auto& param_v2{
|
||||||
*reinterpret_cast<BiquadFilterInfo::ParameterVersion2*>(effect_info.GetParameter())};
|
*reinterpret_cast<BiquadFilterInfo::ParameterVersion2*>(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<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();
|
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()) {
|
// Generate commands for each active channel
|
||||||
for (s8 channel = 0; channel < channels; channel++) {
|
for (s8 channel = 0; channel < channel_count; channel++) {
|
||||||
command_buffer.GenerateBiquadFilterCommand(
|
command_buffer.GenerateBiquadFilterCommand(
|
||||||
node_id, effect_info, buffer_offset, channel, needs_init, use_float_processing);
|
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto& parameter{
|
auto& parameter{
|
||||||
*reinterpret_cast<BiquadFilterInfo::ParameterVersion1*>(effect_info.GetParameter())};
|
*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};
|
bool needs_init{false};
|
||||||
|
|
||||||
switch (parameter.state) {
|
switch (parameter.state) {
|
||||||
|
|
@ -400,8 +455,10 @@ void CommandGenerator::GenerateBiquadFilterEffectCommand(const s16 buffer_offset
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
LOG_ERROR(Service_Audio, "Invalid biquad parameter state {}",
|
// Should not reach here after validation, but handle gracefully
|
||||||
static_cast<u32>(parameter.state));
|
LOG_WARNING(Service_Audio, "BiquadFilterEffectCommand: Unexpected state {}, treating as Updated",
|
||||||
|
static_cast<u8>(parameter.state));
|
||||||
|
needs_init = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -410,11 +467,6 @@ void CommandGenerator::GenerateBiquadFilterEffectCommand(const s16 buffer_offset
|
||||||
node_id, effect_info, buffer_offset, channel, needs_init,
|
node_id, effect_info, buffer_offset, channel, needs_init,
|
||||||
render_context.behavior->UseBiquadFilterFloatProcessing());
|
render_context.behavior->UseBiquadFilterFloatProcessing());
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
for (s8 channel = 0; channel < parameter.channel_count; channel++) {
|
|
||||||
command_buffer.GenerateCopyMixBufferCommand(node_id, effect_info, buffer_offset,
|
|
||||||
channel);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,25 +27,28 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -57,29 +60,30 @@ void ApplyBiquadFilterFloat2(std::span<s32> output, std::span<const s32> input,
|
||||||
constexpr f64 min{std::numeric_limits<s32>::min()};
|
constexpr f64 min{std::numeric_limits<s32>::min()};
|
||||||
constexpr f64 max{std::numeric_limits<s32>::max()};
|
constexpr f64 max{std::numeric_limits<s32>::max()};
|
||||||
|
|
||||||
std::array<f64, 3> b_double{static_cast<f64>(b[0]), static_cast<f64>(b[1]),
|
std::array<f64, 3> b_double{static_cast<f64>(b[0]), static_cast<f64>(b[1]), static_cast<f64>(b[2])};
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -118,15 +122,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
|
||||||
|
|
@ -144,6 +185,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include "audio_core/renderer/effect/biquad_filter.h"
|
#include "audio_core/renderer/effect/biquad_filter.h"
|
||||||
|
|
||||||
namespace AudioCore::Renderer {
|
namespace AudioCore::Renderer {
|
||||||
|
|
@ -11,9 +13,44 @@ void BiquadFilterInfo::Update(BehaviorInfo::ErrorInfo& error_info,
|
||||||
auto params{reinterpret_cast<ParameterVersion1*>(parameter.data())};
|
auto params{reinterpret_cast<ParameterVersion1*>(parameter.data())};
|
||||||
|
|
||||||
std::memcpy(params, in_specific, sizeof(ParameterVersion1));
|
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<u32>(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<u8>(params->state) > static_cast<u8>(EffectInfoBase::ParameterState::Updated)) {
|
||||||
|
LOG_WARNING(Service_Audio, "BiquadFilterInfo: Invalid parameter state {}, disabling effect",
|
||||||
|
static_cast<u8>(params->state));
|
||||||
|
parameters_valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate input/output buffer indices
|
||||||
|
for (s8 i = 0; static_cast<u32>(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<s8>(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<s8>(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;
|
mix_id = in_params.mix_id;
|
||||||
process_order = in_params.process_order;
|
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.error_code = ResultSuccess;
|
||||||
error_info.address = CpuAddr(0);
|
error_info.address = CpuAddr(0);
|
||||||
|
|
@ -25,9 +62,48 @@ void BiquadFilterInfo::Update(BehaviorInfo::ErrorInfo& error_info,
|
||||||
auto params{reinterpret_cast<ParameterVersion2*>(parameter.data())};
|
auto params{reinterpret_cast<ParameterVersion2*>(parameter.data())};
|
||||||
|
|
||||||
std::memcpy(params, in_specific, sizeof(ParameterVersion2));
|
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<u32>(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<u32>(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<s8>(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<s8>(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<u8>(params->state) > static_cast<u8>(EffectInfoBase::ParameterState::Updated)) {
|
||||||
|
LOG_WARNING(Service_Audio, "BiquadFilterInfo: Invalid parameter state {}, disabling effect",
|
||||||
|
static_cast<u8>(params->state));
|
||||||
|
parameters_valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
mix_id = in_params.mix_id;
|
mix_id = in_params.mix_id;
|
||||||
process_order = in_params.process_order;
|
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<u8>(params->state) <= static_cast<u8>(EffectInfoBase::ParameterState::Updated);
|
||||||
|
|
||||||
error_info.error_code = ResultSuccess;
|
error_info.error_code = ResultSuccess;
|
||||||
error_info.address = CpuAddr(0);
|
error_info.address = CpuAddr(0);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
@ -229,11 +230,20 @@ void SinkStream::ProcessAudioOutAndRender(std::span<s16> output_buffer, std::siz
|
||||||
// If the playing buffer has been consumed or has no frames, we need a new one
|
// 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 (playing_buffer.consumed || playing_buffer.frames == 0) {
|
||||||
if (!queue.try_dequeue(playing_buffer)) {
|
if (!queue.try_dequeue(playing_buffer)) {
|
||||||
// If no buffer was available we've underrun, fill the remaining buffer with
|
// If no buffer was available we've underrun, repeat the last frame to maintain
|
||||||
// the last written frame and continue.
|
// 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++) {
|
for (size_t i = frames_written; i < num_frames; i++) {
|
||||||
std::memcpy(&output_buffer[i * frame_size], &last_frame[0], frame_size_bytes);
|
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<s16, 6> 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;
|
frames_written = num_frames;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue