Merge pull request 'audio/am: Refactor audio effect versioning' (#95) from fix/biquad-filter into main

Reviewed-on: https://git.citron-emu.org/Citron/Emulator/pulls/95
This commit is contained in:
Collecting
2026-01-14 04:55:23 +00:00
5 changed files with 80 additions and 124 deletions

View File

@@ -257,50 +257,32 @@ void CommandBuffer::GenerateBiquadFilterCommand(const s32 node_id, EffectInfoBas
const bool needs_init,
const bool use_float_processing) {
auto& cmd{GenerateStart<BiquadFilterCommand, CommandId::BiquadFilter>(node_id)};
const u8* raw = reinterpret_cast<const u8*>(effect_info.GetParameter());
if (raw[0x16] <= 6 && raw[0x17] <= 2) {
const auto& parameter = *reinterpret_cast<const BiquadFilterInfo::ParameterVersion1*>(effect_info.GetParameter());
cmd.input = buffer_offset + parameter.inputs[channel];
cmd.output = buffer_offset + parameter.outputs[channel];
cmd.biquad.b = parameter.b;
cmd.biquad.a = parameter.a;
} else {
const auto& parameter = *reinterpret_cast<const BiquadFilterInfo::ParameterVersion2*>(effect_info.GetParameter());
cmd.input = buffer_offset + parameter.inputs[channel];
cmd.output = buffer_offset + parameter.outputs[channel];
constexpr f32 scale = 16384.0f;
cmd.biquad.b[0] = static_cast<s16>(std::clamp(parameter.b[0] * scale, -32768.0f, 32767.0f));
cmd.biquad.b[1] = static_cast<s16>(std::clamp(parameter.b[1] * scale, -32768.0f, 32767.0f));
cmd.biquad.b[2] = static_cast<s16>(std::clamp(parameter.b[2] * scale, -32768.0f, 32767.0f));
cmd.biquad.a[0] = static_cast<s16>(std::clamp(parameter.a[0] * scale, -32768.0f, 32767.0f));
cmd.biquad.a[1] = static_cast<s16>(std::clamp(parameter.a[1] * scale, -32768.0f, 32767.0f));
}
const auto state{reinterpret_cast<VoiceState::BiquadFilterState*>(
effect_info.GetStateBuffer() + channel * sizeof(VoiceState::BiquadFilterState))};
// Check which parameter version is being used
if (behavior->IsEffectInfoVersion2Supported()) {
const auto& parameter{
*reinterpret_cast<BiquadFilterInfo::ParameterVersion2*>(effect_info.GetParameter())};
cmd.input = buffer_offset + parameter.inputs[channel];
cmd.output = buffer_offset + parameter.outputs[channel];
// Convert float coefficients to fixed-point Q2.14 format (multiply by 16384)
constexpr f32 fixed_point_scale = 16384.0f;
cmd.biquad.b[0] = static_cast<s16>(
std::clamp(parameter.b[0] * fixed_point_scale, -32768.0f, 32767.0f));
cmd.biquad.b[1] = static_cast<s16>(
std::clamp(parameter.b[1] * fixed_point_scale, -32768.0f, 32767.0f));
cmd.biquad.b[2] = static_cast<s16>(
std::clamp(parameter.b[2] * fixed_point_scale, -32768.0f, 32767.0f));
cmd.biquad.a[0] = static_cast<s16>(
std::clamp(parameter.a[0] * fixed_point_scale, -32768.0f, 32767.0f));
cmd.biquad.a[1] = static_cast<s16>(
std::clamp(parameter.a[1] * fixed_point_scale, -32768.0f, 32767.0f));
// Effects use legacy fixed-point format
cmd.use_float_coefficients = false;
} else {
const auto& parameter{
*reinterpret_cast<BiquadFilterInfo::ParameterVersion1*>(effect_info.GetParameter())};
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.state = memory_pool->Translate(CpuAddr(state),
MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState));
cmd.state = memory_pool->Translate(CpuAddr(state), MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState));
cmd.use_float_coefficients = false;
cmd.needs_init = needs_init;
cmd.use_float_processing = use_float_processing;
@@ -749,10 +731,8 @@ void CommandBuffer::GenerateMultitapBiquadFilterCommand(const s32 node_id, Voice
cmd.output = buffer_count + channel;
cmd.biquads = voice_info.biquads;
// REV15+: Use native float coefficients if available
if (voice_info.use_float_biquads) {
cmd.biquads_float = voice_info.biquads_float;
cmd.use_float_coefficients = true;
cmd.use_float_coefficients = false;
} else {
cmd.use_float_coefficients = false;
}

View File

@@ -362,61 +362,63 @@ void CommandGenerator::GenerateAuxCommand(const s16 buffer_offset, EffectInfoBas
void CommandGenerator::GenerateBiquadFilterEffectCommand(const s16 buffer_offset,
EffectInfoBase& effect_info,
const s32 node_id) {
EffectInfoBase::ParameterState state{};
s8 channel_count{0};
u8* raw_params = effect_info.GetParameter();
// REV15 introduced the new native float-based Biquad Filter structure (Version 2).
// REV14 and older (including Rev 12 splitter updates) still use the Int16 version.
const bool is_rev15 = render_context.behavior->GetProcessRevision() >= 15;
if (render_context.behavior->IsEffectInfoVersion2Supported()) {
const auto* parameter = reinterpret_cast<const BiquadFilterInfo::ParameterVersion2*>(
effect_info.GetParameter());
if (!parameter) {
LOG_ERROR(Service_Audio, "Biquad filter parameter is null");
return;
}
state = parameter->state;
channel_count = parameter->channel_count;
} else {
const auto* parameter = reinterpret_cast<const BiquadFilterInfo::ParameterVersion1*>(
effect_info.GetParameter());
if (!parameter) {
LOG_ERROR(Service_Audio, "Biquad filter parameter is null");
return;
}
state = parameter->state;
channel_count = parameter->channel_count;
}
if (is_rev15) {
auto& parameter = *reinterpret_cast<BiquadFilterInfo::ParameterVersion2*>(raw_params);
s8 channels = parameter.channel_count;
if (channels > 6 || channels < 0) channels = 0;
if (effect_info.IsEnabled()) {
bool needs_init{false};
switch (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 = state == EffectInfoBase::ParameterState::Updating;
if (effect_info.IsEnabled()) {
const bool needs_init = (parameter.state != EffectInfoBase::ParameterState::Updated);
for (s8 channel = 0; channel < channels; channel++) {
command_buffer.GenerateBiquadFilterCommand(
node_id, effect_info, buffer_offset, channel, needs_init, true);
}
} else {
for (s8 channel = 0; channel < channels; channel++) {
command_buffer.GenerateCopyMixBufferCommand(node_id, effect_info, buffer_offset, channel);
}
break;
default:
LOG_ERROR(Service_Audio, "Invalid biquad parameter state {}, treating as uninitialized",
static_cast<u32>(state));
needs_init = true;
break;
}
for (s8 channel = 0; channel < channel_count; channel++) {
command_buffer.GenerateBiquadFilterCommand(
node_id, effect_info, buffer_offset, channel, needs_init,
render_context.behavior->UseBiquadFilterFloatProcessing());
}
parameter.state = EffectInfoBase::ParameterState::Updated;
} else {
for (s8 channel = 0; channel < channel_count; channel++) {
command_buffer.GenerateCopyMixBufferCommand(node_id, effect_info, buffer_offset,
channel);
auto& parameter = *reinterpret_cast<BiquadFilterInfo::ParameterVersion1*>(raw_params);
s8 channels = parameter.channel_count;
if (channels > 6 || channels < 0) channels = 0;
if (effect_info.IsEnabled()) {
bool needs_init = false;
switch (parameter.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 = parameter.state == EffectInfoBase::ParameterState::Updating;
}
break;
default: break;
}
for (s8 channel = 0; channel < channels; channel++) {
command_buffer.GenerateBiquadFilterCommand(
node_id, effect_info, buffer_offset, channel, needs_init, false);
}
} else {
for (s8 channel = 0; channel < channels; channel++) {
command_buffer.GenerateCopyMixBufferCommand(node_id, effect_info, buffer_offset, channel);
}
}
parameter.state = EffectInfoBase::ParameterState::Updated;
}
}

View File

@@ -10,12 +10,10 @@ void BiquadFilterInfo::Update(BehaviorInfo::ErrorInfo& error_info,
const InParameterVersion1& in_params, const PoolMapper& pool_mapper) {
auto in_specific{reinterpret_cast<const ParameterVersion1*>(in_params.specific.data())};
auto params{reinterpret_cast<ParameterVersion1*>(parameter.data())};
std::memcpy(params, in_specific, sizeof(ParameterVersion1));
mix_id = in_params.mix_id;
process_order = in_params.process_order;
enabled = in_params.enabled;
error_info.error_code = ResultSuccess;
error_info.address = CpuAddr(0);
}
@@ -24,12 +22,10 @@ void BiquadFilterInfo::Update(BehaviorInfo::ErrorInfo& error_info,
const InParameterVersion2& in_params, const PoolMapper& pool_mapper) {
auto in_specific{reinterpret_cast<const ParameterVersion2*>(in_params.specific.data())};
auto params{reinterpret_cast<ParameterVersion2*>(parameter.data())};
std::memcpy(params, in_specific, sizeof(ParameterVersion2));
mix_id = in_params.mix_id;
process_order = in_params.process_order;
enabled = in_params.enabled;
error_info.error_code = ResultSuccess;
error_info.address = CpuAddr(0);
}
@@ -40,34 +36,10 @@ void BiquadFilterInfo::UpdateForCommandGeneration() {
} else {
usage_state = UsageState::Disabled;
}
// Determine which version structure is being used
// Version 1: state at offset 0x17, structure size ~24 bytes
// Version 2: state at offset 0x25, structure size ~40 bytes
auto params_v1{reinterpret_cast<ParameterVersion1*>(parameter.data())};
auto params_v2{reinterpret_cast<ParameterVersion2*>(parameter.data())};
// Check which state location contains a valid ParameterState value (0-2)
// Valid states: Initialized (0), Updating (1), Updated (2)
const auto state_v1_raw = *reinterpret_cast<const u8*>(&params_v1->state);
const auto state_v2_raw = *reinterpret_cast<const u8*>(&params_v2->state);
if (state_v1_raw <= 2) {
// Version 1 location has valid state, update there
params_v1->state = ParameterState::Updated;
} else if (state_v2_raw <= 2) {
// Version 2 location has valid state, update there
params_v2->state = ParameterState::Updated;
} else {
// Neither looks valid, update both (one will be wrong but command generator handles it)
params_v1->state = ParameterState::Updated;
params_v2->state = ParameterState::Updated;
}
// State update is moved to Generator to handle version-specific offsets.
}
void BiquadFilterInfo::InitializeResultState(EffectResultState& result_state) {}
void BiquadFilterInfo::UpdateResultState(EffectResultState& cpu_state,
EffectResultState& dsp_state) {}
void BiquadFilterInfo::UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) {}
} // namespace AudioCore::Renderer

View File

@@ -35,8 +35,7 @@ public:
/* 0x25 */ ParameterState state;
/* 0x26 */ u16 reserved;
};
static_assert(sizeof(ParameterVersion2) <= sizeof(EffectInfoBase::InParameterVersion2),
"BiquadFilterInfo::ParameterVersion2 has the wrong size!");
static_assert(sizeof(ParameterVersion2) <= 0x28, "BiquadFilterInfo::ParameterVersion2 has the wrong size!");
/**
* Update the info with new parameters, version 1.

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@@ -207,6 +208,8 @@ public:
return state.data();
}
size_t GetParameterSize() const { return parameter.size(); }
/**
* Set this effect's usage state.
*