mirror of
https://git.citron-emu.org/citron/emulator
synced 2026-01-19 09:23:50 +00:00
Merge pull request 'audio_core: Add Audio Renderer REV12 and REV13 support' (#85) from feature/audio-renderer-rev12-rev13 into main
Reviewed-on: https://git.citron-emu.org/Citron/Emulator/pulls/85
This commit is contained in:
@@ -101,6 +101,10 @@ add_library(audio_core STATIC
|
||||
renderer/command/effect/limiter.h
|
||||
renderer/command/effect/multi_tap_biquad_filter.cpp
|
||||
renderer/command/effect/multi_tap_biquad_filter.h
|
||||
renderer/command/effect/biquad_filter_and_mix.cpp
|
||||
renderer/command/effect/biquad_filter_and_mix.h
|
||||
renderer/command/effect/multi_tap_biquad_filter_and_mix.cpp
|
||||
renderer/command/effect/multi_tap_biquad_filter_and_mix.h
|
||||
renderer/command/effect/reverb.cpp
|
||||
renderer/command/effect/reverb.h
|
||||
renderer/command/mix/clear_mix.cpp
|
||||
|
||||
@@ -49,6 +49,7 @@ enum class SupportTags {
|
||||
SplitterPrevVolumeReset,
|
||||
SplitterDestinationV2b,
|
||||
VoiceInParameterV2,
|
||||
SplitterBiquadFilter,
|
||||
|
||||
// Not a real tag, just here to get the count.
|
||||
Size
|
||||
@@ -95,6 +96,7 @@ constexpr bool CheckFeatureSupported(SupportTags tag, u32 user_revision) {
|
||||
{SupportTags::CompressorStatistics, 13},
|
||||
{SupportTags::SplitterPrevVolumeReset, 13},
|
||||
{SupportTags::DeviceApiVersion2, 13},
|
||||
{SupportTags::SplitterBiquadFilter, 12},
|
||||
{SupportTags::SplitterDestinationV2b, 15},
|
||||
{SupportTags::VoiceInParameterV2, 15},
|
||||
}};
|
||||
|
||||
@@ -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
|
||||
|
||||
#include "audio_core/common/feature_support.h"
|
||||
@@ -206,4 +207,8 @@ bool BehaviorInfo::IsVoiceInParameterV2Supported() const {
|
||||
return CheckFeatureSupported(SupportTags::VoiceInParameterV2, user_revision);
|
||||
}
|
||||
|
||||
bool BehaviorInfo::IsBiquadFilterParameterForSplitterEnabled() const {
|
||||
return CheckFeatureSupported(SupportTags::SplitterBiquadFilter, user_revision);
|
||||
}
|
||||
|
||||
} // namespace AudioCore::Renderer
|
||||
|
||||
@@ -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
|
||||
@@ -394,6 +395,14 @@ public:
|
||||
*/
|
||||
bool IsVoiceInParameterV2Supported() const;
|
||||
|
||||
/**
|
||||
* Check if biquad filter parameters for splitter destinations are supported (revision 12+).
|
||||
* This allows splitter destinations to specify up to 2 biquad filters for filtering before mixing.
|
||||
*
|
||||
* @return True if supported, otherwise false.
|
||||
*/
|
||||
bool IsBiquadFilterParameterForSplitterEnabled() const;
|
||||
|
||||
/// Host version
|
||||
u32 process_revision;
|
||||
/// User version
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include "audio_core/common/common.h"
|
||||
#include "audio_core/common/feature_support.h"
|
||||
#include "audio_core/renderer/behavior/behavior_info.h"
|
||||
#include "audio_core/renderer/behavior/info_updater.h"
|
||||
@@ -351,10 +352,33 @@ Result InfoUpdater::UpdateMixes(MixContext& mix_context, const u32 mix_buffer_co
|
||||
u32 consumed_input_size{0};
|
||||
u32 input_mix_size{0};
|
||||
|
||||
LOG_DEBUG(Service_Audio,
|
||||
"UpdateMixes: mix_size={}, dirty_update_supported={}, mix_context_count={}, "
|
||||
"input_magic=0x{:X}",
|
||||
in_header->mix_size, behaviour.IsMixInParameterDirtyOnlyUpdateSupported(),
|
||||
mix_context.GetCount(),
|
||||
input[0] | (input[1] << 8) | (input[2] << 16) | (input[3] << 24));
|
||||
|
||||
// Check if we're seeing splitter destination data instead of mix data
|
||||
const u32 input_magic = input[0] | (input[1] << 8) | (input[2] << 16) | (input[3] << 24);
|
||||
if (input_magic == AudioCore::GetSplitterSendDataMagic()) {
|
||||
LOG_ERROR(Service_Audio,
|
||||
"UpdateMixes: Found splitter destination data (magic=0x{:X}) instead of mix data. "
|
||||
"This suggests UpdateSplitterInfo didn't consume the splitter data correctly. "
|
||||
"offset_from_start={}",
|
||||
input_magic, CpuAddr(input) - CpuAddr(input_origin.data()));
|
||||
// Don't return error here - let it fail naturally so we can see the full error
|
||||
}
|
||||
|
||||
if (behaviour.IsMixInParameterDirtyOnlyUpdateSupported()) {
|
||||
auto in_dirty_params{reinterpret_cast<const MixInfo::InDirtyParameter*>(input)};
|
||||
mix_count = in_dirty_params->count;
|
||||
|
||||
LOG_DEBUG(Service_Audio,
|
||||
"Dirty parameter: count={}, magic=0x{:X}, expected_size={}, offset_from_start={}",
|
||||
mix_count, in_dirty_params->magic, in_header->mix_size,
|
||||
CpuAddr(input) - CpuAddr(input_origin.data()));
|
||||
|
||||
// Validate against expected header size to ensure structure is correct
|
||||
if (mix_count < 0 || mix_count > 0x100) {
|
||||
LOG_ERROR(Service_Audio,
|
||||
@@ -430,9 +454,16 @@ Result InfoUpdater::UpdateMixes(MixContext& mix_context, const u32 mix_buffer_co
|
||||
}
|
||||
}
|
||||
|
||||
LOG_DEBUG(Service_Audio,
|
||||
"UpdateMixes: consumed={}, header_size={}, mix_count={}, mix_param_size={}",
|
||||
consumed_input_size, in_header->mix_size, mix_count, sizeof(MixInfo::InParameter));
|
||||
|
||||
if (consumed_input_size != in_header->mix_size) {
|
||||
LOG_ERROR(Service_Audio, "Consumed an incorrect mixes size, header size={}, consumed={}",
|
||||
in_header->mix_size, consumed_input_size);
|
||||
LOG_ERROR(Service_Audio,
|
||||
"Consumed an incorrect mixes size, header size={}, consumed={}, "
|
||||
"mix_count={}, mix_param_size={}, offset_from_start={}",
|
||||
in_header->mix_size, consumed_input_size, mix_count,
|
||||
sizeof(MixInfo::InParameter), CpuAddr(input) - CpuAddr(input_origin.data()));
|
||||
return Service::Audio::ResultInvalidUpdateInfo;
|
||||
}
|
||||
|
||||
@@ -604,11 +635,22 @@ Result InfoUpdater::UpdateErrorInfo(const BehaviorInfo& behaviour_) {
|
||||
}
|
||||
|
||||
Result InfoUpdater::UpdateSplitterInfo(SplitterContext& splitter_context) {
|
||||
LOG_DEBUG(Service_Audio,
|
||||
"UpdateSplitterInfo: offset_from_start={}, input_magic=0x{:X}",
|
||||
CpuAddr(input) - CpuAddr(input_origin.data()),
|
||||
input[0] | (input[1] << 8) | (input[2] << 16) | (input[3] << 24));
|
||||
|
||||
u32 consumed_size{0};
|
||||
if (!splitter_context.Update(input, consumed_size)) {
|
||||
LOG_ERROR(Service_Audio, "UpdateSplitterInfo: splitter_context.Update failed, consumed_size={}",
|
||||
consumed_size);
|
||||
return Service::Audio::ResultInvalidUpdateInfo;
|
||||
}
|
||||
|
||||
LOG_DEBUG(Service_Audio,
|
||||
"UpdateSplitterInfo: consumed={}, new_offset_from_start={}",
|
||||
consumed_size, CpuAddr(input) - CpuAddr(input_origin.data()) + consumed_size);
|
||||
|
||||
input += consumed_size;
|
||||
|
||||
return ResultSuccess;
|
||||
|
||||
@@ -363,6 +363,62 @@ void CommandBuffer::GenerateMixRampGroupedCommand(const s32 node_id, const s16 b
|
||||
GenerateEnd<MixRampGroupedCommand>(cmd);
|
||||
}
|
||||
|
||||
void CommandBuffer::GenerateBiquadFilterAndMix(
|
||||
s32 node_id, f32 volume0, f32 volume1, s16 input_index, s16 output_index,
|
||||
s32 last_sample_index, CpuAddr voice_state_addr,
|
||||
const VoiceInfo::BiquadFilterParameter& filter, CpuAddr biquad_state,
|
||||
CpuAddr previous_biquad_state, bool need_init, bool has_volume_ramp,
|
||||
bool is_first_mix_buffer) {
|
||||
auto& cmd{GenerateStart<BiquadFilterAndMixCommand, CommandId::BiquadFilterAndMix>(node_id)};
|
||||
|
||||
cmd.input = input_index;
|
||||
cmd.output = output_index;
|
||||
cmd.biquad = filter;
|
||||
cmd.state = memory_pool->Translate(biquad_state, sizeof(VoiceState::BiquadFilterState));
|
||||
cmd.previous_state =
|
||||
memory_pool->Translate(previous_biquad_state, sizeof(VoiceState::BiquadFilterState));
|
||||
cmd.voice_state = memory_pool->Translate(voice_state_addr, sizeof(VoiceState));
|
||||
cmd.last_sample_index = last_sample_index;
|
||||
cmd.volume0 = volume0;
|
||||
cmd.volume1 = volume1;
|
||||
cmd.needs_init = need_init;
|
||||
cmd.has_volume_ramp = has_volume_ramp;
|
||||
cmd.is_first_mix_buffer = is_first_mix_buffer;
|
||||
|
||||
GenerateEnd<BiquadFilterAndMixCommand>(cmd);
|
||||
}
|
||||
|
||||
void CommandBuffer::GenerateMultiTapBiquadFilterAndMix(
|
||||
s32 node_id, f32 volume0, f32 volume1, s16 input_index, s16 output_index,
|
||||
s32 last_sample_index, CpuAddr voice_state_addr,
|
||||
const std::array<VoiceInfo::BiquadFilterParameter, MaxBiquadFilters>& filters,
|
||||
const std::array<CpuAddr, MaxBiquadFilters>& biquad_states,
|
||||
const std::array<CpuAddr, MaxBiquadFilters>& previous_biquad_states,
|
||||
const std::array<bool, MaxBiquadFilters>& needs_init, bool has_volume_ramp,
|
||||
bool is_first_mix_buffer) {
|
||||
auto& cmd{GenerateStart<MultiTapBiquadFilterAndMixCommand,
|
||||
CommandId::MultiTapBiquadFilterAndMix>(node_id)};
|
||||
|
||||
cmd.input = input_index;
|
||||
cmd.output = output_index;
|
||||
cmd.biquads = filters;
|
||||
for (u32 i = 0; i < MaxBiquadFilters; i++) {
|
||||
cmd.states[i] =
|
||||
memory_pool->Translate(biquad_states[i], sizeof(VoiceState::BiquadFilterState));
|
||||
cmd.previous_states[i] = memory_pool->Translate(previous_biquad_states[i],
|
||||
sizeof(VoiceState::BiquadFilterState));
|
||||
cmd.needs_init[i] = needs_init[i];
|
||||
}
|
||||
cmd.voice_state = memory_pool->Translate(voice_state_addr, sizeof(VoiceState));
|
||||
cmd.last_sample_index = last_sample_index;
|
||||
cmd.volume0 = volume0;
|
||||
cmd.volume1 = volume1;
|
||||
cmd.has_volume_ramp = has_volume_ramp;
|
||||
cmd.is_first_mix_buffer = is_first_mix_buffer;
|
||||
|
||||
GenerateEnd<MultiTapBiquadFilterAndMixCommand>(cmd);
|
||||
}
|
||||
|
||||
void CommandBuffer::GenerateDepopPrepareCommand(const s32 node_id, const VoiceState& voice_state,
|
||||
std::span<const s32> buffer, const s16 buffer_count,
|
||||
s16 buffer_offset, const bool was_playing) {
|
||||
|
||||
@@ -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
|
||||
@@ -217,6 +218,56 @@ public:
|
||||
std::span<const f32> prev_volumes, CpuAddr prev_samples,
|
||||
u8 precision);
|
||||
|
||||
/**
|
||||
* Generate a biquad filter and mix command (REV12+).
|
||||
*
|
||||
* @param node_id - Node id of the voice this command is generated for.
|
||||
* @param volume0 - Initial volume (for ramp).
|
||||
* @param volume1 - Final volume.
|
||||
* @param input_index - Input mix buffer index.
|
||||
* @param output_index - Output mix buffer index.
|
||||
* @param last_sample_index - Index in voice state last_samples array.
|
||||
* @param voice_state_addr - Voice state address.
|
||||
* @param filter - Biquad filter parameter.
|
||||
* @param biquad_state - Biquad filter state address.
|
||||
* @param previous_biquad_state - Previous biquad filter state address.
|
||||
* @param need_init - If true, reset the state.
|
||||
* @param has_volume_ramp - If true, use volume ramp.
|
||||
* @param is_first_mix_buffer - If true, this is the first mix buffer.
|
||||
*/
|
||||
void GenerateBiquadFilterAndMix(s32 node_id, f32 volume0, f32 volume1, s16 input_index,
|
||||
s16 output_index, s32 last_sample_index,
|
||||
CpuAddr voice_state_addr,
|
||||
const VoiceInfo::BiquadFilterParameter& filter,
|
||||
CpuAddr biquad_state, CpuAddr previous_biquad_state,
|
||||
bool need_init, bool has_volume_ramp, bool is_first_mix_buffer);
|
||||
|
||||
/**
|
||||
* Generate a multi-tap biquad filter and mix command (REV12+).
|
||||
*
|
||||
* @param node_id - Node id of the voice this command is generated for.
|
||||
* @param volume0 - Initial volume (for ramp).
|
||||
* @param volume1 - Final volume.
|
||||
* @param input_index - Input mix buffer index.
|
||||
* @param output_index - Output mix buffer index.
|
||||
* @param last_sample_index - Index in voice state last_samples array.
|
||||
* @param voice_state_addr - Voice state address.
|
||||
* @param filters - Array of two biquad filter parameters.
|
||||
* @param biquad_states - Array of two biquad filter state addresses.
|
||||
* @param previous_biquad_states - Array of two previous biquad filter state addresses.
|
||||
* @param needs_init - Array indicating if each filter needs initialization.
|
||||
* @param has_volume_ramp - If true, use volume ramp.
|
||||
* @param is_first_mix_buffer - If true, this is the first mix buffer.
|
||||
*/
|
||||
void GenerateMultiTapBiquadFilterAndMix(
|
||||
s32 node_id, f32 volume0, f32 volume1, s16 input_index, s16 output_index,
|
||||
s32 last_sample_index, CpuAddr voice_state_addr,
|
||||
const std::array<VoiceInfo::BiquadFilterParameter, MaxBiquadFilters>& filters,
|
||||
const std::array<CpuAddr, MaxBiquadFilters>& biquad_states,
|
||||
const std::array<CpuAddr, MaxBiquadFilters>& previous_biquad_states,
|
||||
const std::array<bool, MaxBiquadFilters>& needs_init, bool has_volume_ramp,
|
||||
bool is_first_mix_buffer);
|
||||
|
||||
/**
|
||||
* Generate a depop prepare command, adding it to the command list.
|
||||
*
|
||||
|
||||
@@ -622,13 +622,105 @@ void CommandGenerator::GenerateMixCommands(MixInfo& mix_info) {
|
||||
auto splitter_mix_info{mix_context.GetInfo(splitter_mix_id)};
|
||||
const s16 input_index{static_cast<s16>(mix_info.buffer_offset +
|
||||
(dest_id % mix_info.buffer_count))};
|
||||
for (s16 i = 0; i < splitter_mix_info->buffer_count; i++) {
|
||||
auto volume{mix_info.volume * destination->GetMixVolume(i)};
|
||||
if (volume != 0.0f) {
|
||||
command_buffer.GenerateMixCommand(
|
||||
mix_info.node_id, input_index,
|
||||
splitter_mix_info->buffer_offset + i, mix_info.buffer_offset,
|
||||
volume, precision);
|
||||
|
||||
// REV12+: Check if destination has biquad filters
|
||||
if (render_context.behavior->IsBiquadFilterParameterForSplitterEnabled()) {
|
||||
auto bqf_state = splitter_context.GetBiquadFilterState(destination->GetId());
|
||||
auto bqf_filters = destination->GetBiquadFiltersRev12();
|
||||
|
||||
// Count enabled filters
|
||||
u32 enabled_count = 0;
|
||||
for (const auto& filter : bqf_filters) {
|
||||
if (filter.enabled) {
|
||||
enabled_count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (enabled_count > 0 &&
|
||||
bqf_state.size() >= SplitterContext::BqfStatesPerDestination) {
|
||||
// Use combined filter+mix commands
|
||||
for (s16 i = 0; i < splitter_mix_info->buffer_count; i++) {
|
||||
auto volume{mix_info.volume * destination->GetMixVolume(i)};
|
||||
auto prev_volume{
|
||||
mix_info.volume * destination->GetMixVolumePrev(i)};
|
||||
bool has_ramp{volume != prev_volume};
|
||||
|
||||
if (volume != 0.0f || prev_volume != 0.0f) {
|
||||
// States are stored as: filter0_state, filter0_prev, filter1_state, filter1_prev
|
||||
if (enabled_count == 2) {
|
||||
// Multi-tap biquad filter and mix
|
||||
std::array<VoiceInfo::BiquadFilterParameter, MaxBiquadFilters> filters{};
|
||||
std::array<CpuAddr, MaxBiquadFilters> states{};
|
||||
std::array<CpuAddr, MaxBiquadFilters> prev_states{};
|
||||
std::array<bool, MaxBiquadFilters> needs_init{};
|
||||
|
||||
filters[0] = bqf_filters[0];
|
||||
filters[1] = bqf_filters[1];
|
||||
// Filter0: state at index 0, prev at index 1
|
||||
states[0] = CpuAddr(&bqf_state[0]);
|
||||
prev_states[0] = CpuAddr(&bqf_state[1]);
|
||||
// Filter1: state at index 2, prev at index 3
|
||||
states[1] = CpuAddr(&bqf_state[2]);
|
||||
prev_states[1] = CpuAddr(&bqf_state[3]);
|
||||
needs_init[0] = !bqf_filters[0].enabled;
|
||||
needs_init[1] = !bqf_filters[1].enabled;
|
||||
|
||||
command_buffer.GenerateMultiTapBiquadFilterAndMix(
|
||||
mix_info.node_id, prev_volume, volume, input_index,
|
||||
splitter_mix_info->buffer_offset + i, -1,
|
||||
CpuAddr(0), filters, states, prev_states, needs_init,
|
||||
has_ramp, dest_id == 0 && i == 0);
|
||||
} else {
|
||||
// Single biquad filter and mix
|
||||
VoiceInfo::BiquadFilterParameter filter{};
|
||||
CpuAddr state_addr{};
|
||||
CpuAddr prev_state_addr{};
|
||||
bool need_init{true};
|
||||
|
||||
if (bqf_filters[0].enabled) {
|
||||
filter = bqf_filters[0];
|
||||
// Filter0: state at index 0, prev at index 1
|
||||
state_addr = CpuAddr(&bqf_state[0]);
|
||||
prev_state_addr = CpuAddr(&bqf_state[1]);
|
||||
need_init = false;
|
||||
} else if (bqf_filters[1].enabled) {
|
||||
filter = bqf_filters[1];
|
||||
// Filter1: state at index 2, prev at index 3
|
||||
state_addr = CpuAddr(&bqf_state[2]);
|
||||
prev_state_addr = CpuAddr(&bqf_state[3]);
|
||||
need_init = false;
|
||||
}
|
||||
|
||||
command_buffer.GenerateBiquadFilterAndMix(
|
||||
mix_info.node_id, prev_volume, volume, input_index,
|
||||
splitter_mix_info->buffer_offset + i, -1,
|
||||
CpuAddr(0), filter, state_addr, prev_state_addr,
|
||||
need_init, has_ramp, dest_id == 0 && i == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No filters, use regular mix
|
||||
for (s16 i = 0; i < splitter_mix_info->buffer_count; i++) {
|
||||
auto volume{mix_info.volume * destination->GetMixVolume(i)};
|
||||
if (volume != 0.0f) {
|
||||
command_buffer.GenerateMixCommand(
|
||||
mix_info.node_id, input_index,
|
||||
splitter_mix_info->buffer_offset + i,
|
||||
mix_info.buffer_offset, volume, precision);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// REV11 or earlier: use regular mix
|
||||
for (s16 i = 0; i < splitter_mix_info->buffer_count; i++) {
|
||||
auto volume{mix_info.volume * destination->GetMixVolume(i)};
|
||||
if (volume != 0.0f) {
|
||||
command_buffer.GenerateMixCommand(
|
||||
mix_info.node_id, input_index,
|
||||
splitter_mix_info->buffer_offset + i, mix_info.buffer_offset,
|
||||
volume, precision);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
#include "audio_core/renderer/command/command_processing_time_estimator.h"
|
||||
@@ -3617,4 +3618,58 @@ u32 CommandProcessingTimeEstimatorVersion5::Estimate(const CompressorCommand& co
|
||||
}
|
||||
}
|
||||
|
||||
// BiquadFilterAndMixCommand estimates (REV12+)
|
||||
// Similar to BiquadFilter + Mix, but slightly optimized as combined operation
|
||||
u32 CommandProcessingTimeEstimatorVersion1::Estimate(
|
||||
[[maybe_unused]] const BiquadFilterAndMixCommand& command) const {
|
||||
return static_cast<u32>((static_cast<f32>(sample_count) * 60.0f) * 1.2f);
|
||||
}
|
||||
|
||||
u32 CommandProcessingTimeEstimatorVersion2::Estimate(
|
||||
[[maybe_unused]] const BiquadFilterAndMixCommand& command) const {
|
||||
return static_cast<u32>((static_cast<f32>(sample_count) * 60.0f) * 1.2f);
|
||||
}
|
||||
|
||||
u32 CommandProcessingTimeEstimatorVersion3::Estimate(
|
||||
[[maybe_unused]] const BiquadFilterAndMixCommand& command) const {
|
||||
return static_cast<u32>((static_cast<f32>(sample_count) * 60.0f) * 1.2f);
|
||||
}
|
||||
|
||||
u32 CommandProcessingTimeEstimatorVersion4::Estimate(
|
||||
[[maybe_unused]] const BiquadFilterAndMixCommand& command) const {
|
||||
return static_cast<u32>((static_cast<f32>(sample_count) * 60.0f) * 1.2f);
|
||||
}
|
||||
|
||||
u32 CommandProcessingTimeEstimatorVersion5::Estimate(
|
||||
[[maybe_unused]] const BiquadFilterAndMixCommand& command) const {
|
||||
return static_cast<u32>((static_cast<f32>(sample_count) * 60.0f) * 1.2f);
|
||||
}
|
||||
|
||||
// MultiTapBiquadFilterAndMixCommand estimates (REV12+)
|
||||
// Similar to MultiTapBiquadFilter + Mix, but slightly optimized as combined operation
|
||||
u32 CommandProcessingTimeEstimatorVersion1::Estimate(
|
||||
[[maybe_unused]] const MultiTapBiquadFilterAndMixCommand& command) const {
|
||||
return static_cast<u32>((static_cast<f32>(sample_count) * 100.0f) * 1.2f);
|
||||
}
|
||||
|
||||
u32 CommandProcessingTimeEstimatorVersion2::Estimate(
|
||||
[[maybe_unused]] const MultiTapBiquadFilterAndMixCommand& command) const {
|
||||
return static_cast<u32>((static_cast<f32>(sample_count) * 100.0f) * 1.2f);
|
||||
}
|
||||
|
||||
u32 CommandProcessingTimeEstimatorVersion3::Estimate(
|
||||
[[maybe_unused]] const MultiTapBiquadFilterAndMixCommand& command) const {
|
||||
return static_cast<u32>((static_cast<f32>(sample_count) * 100.0f) * 1.2f);
|
||||
}
|
||||
|
||||
u32 CommandProcessingTimeEstimatorVersion4::Estimate(
|
||||
[[maybe_unused]] const MultiTapBiquadFilterAndMixCommand& command) const {
|
||||
return static_cast<u32>((static_cast<f32>(sample_count) * 100.0f) * 1.2f);
|
||||
}
|
||||
|
||||
u32 CommandProcessingTimeEstimatorVersion5::Estimate(
|
||||
[[maybe_unused]] const MultiTapBiquadFilterAndMixCommand& command) const {
|
||||
return static_cast<u32>((static_cast<f32>(sample_count) * 100.0f) * 1.2f);
|
||||
}
|
||||
|
||||
} // namespace AudioCore::Renderer
|
||||
|
||||
@@ -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
|
||||
@@ -44,6 +45,8 @@ public:
|
||||
virtual u32 Estimate(const MultiTapBiquadFilterCommand& command) const = 0;
|
||||
virtual u32 Estimate(const CaptureCommand& command) const = 0;
|
||||
virtual u32 Estimate(const CompressorCommand& command) const = 0;
|
||||
virtual u32 Estimate(const BiquadFilterAndMixCommand& command) const = 0;
|
||||
virtual u32 Estimate(const MultiTapBiquadFilterAndMixCommand& command) const = 0;
|
||||
};
|
||||
|
||||
class CommandProcessingTimeEstimatorVersion1 final : public ICommandProcessingTimeEstimator {
|
||||
@@ -81,6 +84,8 @@ public:
|
||||
u32 Estimate(const MultiTapBiquadFilterCommand& command) const override;
|
||||
u32 Estimate(const CaptureCommand& command) const override;
|
||||
u32 Estimate(const CompressorCommand& command) const override;
|
||||
u32 Estimate(const BiquadFilterAndMixCommand& command) const override;
|
||||
u32 Estimate(const MultiTapBiquadFilterAndMixCommand& command) const override;
|
||||
|
||||
private:
|
||||
u32 sample_count{};
|
||||
@@ -122,6 +127,8 @@ public:
|
||||
u32 Estimate(const MultiTapBiquadFilterCommand& command) const override;
|
||||
u32 Estimate(const CaptureCommand& command) const override;
|
||||
u32 Estimate(const CompressorCommand& command) const override;
|
||||
u32 Estimate(const BiquadFilterAndMixCommand& command) const override;
|
||||
u32 Estimate(const MultiTapBiquadFilterAndMixCommand& command) const override;
|
||||
|
||||
private:
|
||||
u32 sample_count{};
|
||||
@@ -163,6 +170,8 @@ public:
|
||||
u32 Estimate(const MultiTapBiquadFilterCommand& command) const override;
|
||||
u32 Estimate(const CaptureCommand& command) const override;
|
||||
u32 Estimate(const CompressorCommand& command) const override;
|
||||
u32 Estimate(const BiquadFilterAndMixCommand& command) const override;
|
||||
u32 Estimate(const MultiTapBiquadFilterAndMixCommand& command) const override;
|
||||
|
||||
private:
|
||||
u32 sample_count{};
|
||||
@@ -204,6 +213,8 @@ public:
|
||||
u32 Estimate(const MultiTapBiquadFilterCommand& command) const override;
|
||||
u32 Estimate(const CaptureCommand& command) const override;
|
||||
u32 Estimate(const CompressorCommand& command) const override;
|
||||
u32 Estimate(const BiquadFilterAndMixCommand& command) const override;
|
||||
u32 Estimate(const MultiTapBiquadFilterAndMixCommand& command) const override;
|
||||
|
||||
private:
|
||||
u32 sample_count{};
|
||||
@@ -245,6 +256,8 @@ public:
|
||||
u32 Estimate(const MultiTapBiquadFilterCommand& command) const override;
|
||||
u32 Estimate(const CaptureCommand& command) const override;
|
||||
u32 Estimate(const CompressorCommand& command) const override;
|
||||
u32 Estimate(const BiquadFilterAndMixCommand& command) const override;
|
||||
u32 Estimate(const MultiTapBiquadFilterAndMixCommand& command) const override;
|
||||
|
||||
private:
|
||||
u32 sample_count{};
|
||||
|
||||
@@ -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
|
||||
@@ -8,12 +9,14 @@
|
||||
#include "audio_core/renderer/command/data_source/pcm_int16.h"
|
||||
#include "audio_core/renderer/command/effect/aux_.h"
|
||||
#include "audio_core/renderer/command/effect/biquad_filter.h"
|
||||
#include "audio_core/renderer/command/effect/biquad_filter_and_mix.h"
|
||||
#include "audio_core/renderer/command/effect/capture.h"
|
||||
#include "audio_core/renderer/command/effect/compressor.h"
|
||||
#include "audio_core/renderer/command/effect/delay.h"
|
||||
#include "audio_core/renderer/command/effect/i3dl2_reverb.h"
|
||||
#include "audio_core/renderer/command/effect/light_limiter.h"
|
||||
#include "audio_core/renderer/command/effect/multi_tap_biquad_filter.h"
|
||||
#include "audio_core/renderer/command/effect/multi_tap_biquad_filter_and_mix.h"
|
||||
#include "audio_core/renderer/command/effect/reverb.h"
|
||||
#include "audio_core/renderer/command/icommand.h"
|
||||
#include "audio_core/renderer/command/mix/clear_mix.h"
|
||||
|
||||
@@ -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
|
||||
|
||||
#include "audio_core/adsp/apps/audio_renderer/command_list_processor.h"
|
||||
@@ -81,6 +82,264 @@ void ApplyBiquadFilterFloat2(std::span<s32> output, std::span<const s32> input,
|
||||
state.s3 = Common::BitCast<s64>(s[3]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a single biquad filter and mix the result into the output buffer (REV12+).
|
||||
*
|
||||
* @param output - Output container to mix filtered samples into.
|
||||
* @param input - Input container for samples to be filtered.
|
||||
* @param b - Feedforward coefficients.
|
||||
* @param a - Feedback coefficients.
|
||||
* @param state - State to track previous samples between calls.
|
||||
* @param sample_count - Number of samples to process.
|
||||
* @param volume - Mix volume.
|
||||
*/
|
||||
void ApplyBiquadFilterAndMix(std::span<s32> output, std::span<const s32> input,
|
||||
std::array<s16, 3>& b_, std::array<s16, 2>& a_,
|
||||
VoiceState::BiquadFilterState& state, const u32 sample_count,
|
||||
f32 volume) {
|
||||
constexpr f64 min{std::numeric_limits<s32>::min()};
|
||||
constexpr f64 max{std::numeric_limits<s32>::max()};
|
||||
std::array<f64, 3> b{Common::FixedPoint<50, 14>::from_base(b_[0]).to_double(),
|
||||
Common::FixedPoint<50, 14>::from_base(b_[1]).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(),
|
||||
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)};
|
||||
|
||||
for (u32 i = 0; i < sample_count; i++) {
|
||||
f64 in_sample{static_cast<f64>(input[i])};
|
||||
auto filtered{in_sample * b[0] + s[0] * b[1] + s[1] * b[2] + s[2] * a[0] + s[3] * a[1]};
|
||||
|
||||
s[1] = s[0];
|
||||
s[0] = in_sample;
|
||||
s[3] = s[2];
|
||||
s[2] = filtered;
|
||||
|
||||
// Mix into output (additive)
|
||||
f64 mixed{static_cast<f64>(output[i]) + filtered * static_cast<f64>(volume)};
|
||||
output[i] = static_cast<s32>(std::clamp(mixed, min, max));
|
||||
}
|
||||
|
||||
state.s0 = Common::BitCast<s64>(s[0]);
|
||||
state.s1 = Common::BitCast<s64>(s[1]);
|
||||
state.s2 = Common::BitCast<s64>(s[2]);
|
||||
state.s3 = Common::BitCast<s64>(s[3]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a single biquad filter and mix the result into the output buffer with volume ramp (REV12+).
|
||||
*
|
||||
* @param output - Output container to mix filtered samples into.
|
||||
* @param input - Input container for samples to be filtered.
|
||||
* @param b - Feedforward coefficients.
|
||||
* @param a - Feedback coefficients.
|
||||
* @param state - State to track previous samples between calls.
|
||||
* @param sample_count - Number of samples to process.
|
||||
* @param volume - Initial mix volume.
|
||||
* @param ramp - Volume increment step per sample.
|
||||
* @return Last filtered sample value.
|
||||
*/
|
||||
f32 ApplyBiquadFilterAndMixRamp(std::span<s32> output, std::span<const s32> input,
|
||||
std::array<s16, 3>& b_, std::array<s16, 2>& a_,
|
||||
VoiceState::BiquadFilterState& state, const u32 sample_count,
|
||||
f32 volume, f32 ramp) {
|
||||
constexpr f64 min{std::numeric_limits<s32>::min()};
|
||||
constexpr f64 max{std::numeric_limits<s32>::max()};
|
||||
std::array<f64, 3> b{Common::FixedPoint<50, 14>::from_base(b_[0]).to_double(),
|
||||
Common::FixedPoint<50, 14>::from_base(b_[1]).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(),
|
||||
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)};
|
||||
|
||||
f32 current_volume{volume};
|
||||
f32 last_mixed{0.0f};
|
||||
|
||||
for (u32 i = 0; i < sample_count; i++) {
|
||||
f64 in_sample{static_cast<f64>(input[i])};
|
||||
auto filtered{in_sample * b[0] + s[0] * b[1] + s[1] * b[2] + s[2] * a[0] + s[3] * a[1]};
|
||||
|
||||
s[1] = s[0];
|
||||
s[0] = in_sample;
|
||||
s[3] = s[2];
|
||||
s[2] = filtered;
|
||||
|
||||
// Mix into output with current volume
|
||||
last_mixed = static_cast<f32>(filtered * static_cast<f64>(current_volume));
|
||||
f64 mixed{static_cast<f64>(output[i]) + static_cast<f64>(last_mixed)};
|
||||
output[i] = static_cast<s32>(std::clamp(mixed, min, max));
|
||||
|
||||
current_volume += ramp;
|
||||
}
|
||||
|
||||
state.s0 = Common::BitCast<s64>(s[0]);
|
||||
state.s1 = Common::BitCast<s64>(s[1]);
|
||||
state.s2 = Common::BitCast<s64>(s[2]);
|
||||
state.s3 = Common::BitCast<s64>(s[3]);
|
||||
|
||||
return last_mixed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply double biquad filter and mix the result into the output buffer (REV12+).
|
||||
*
|
||||
* @param output - Output container to mix filtered samples into.
|
||||
* @param input - Input container for samples to be filtered.
|
||||
* @param biquads - Array of two biquad filter parameters.
|
||||
* @param states - Array of two biquad filter states.
|
||||
* @param sample_count - Number of samples to process.
|
||||
* @param volume - Mix volume.
|
||||
*/
|
||||
void ApplyDoubleBiquadFilterAndMix(std::span<s32> output, std::span<const s32> input,
|
||||
std::array<VoiceInfo::BiquadFilterParameter, 2>& biquads,
|
||||
std::array<VoiceState::BiquadFilterState, 2>& states,
|
||||
const u32 sample_count, f32 volume) {
|
||||
constexpr f64 min{std::numeric_limits<s32>::min()};
|
||||
constexpr f64 max{std::numeric_limits<s32>::max()};
|
||||
|
||||
// Convert first filter coefficients
|
||||
std::array<f64, 3> b0{Common::FixedPoint<50, 14>::from_base(biquads[0].b[0]).to_double(),
|
||||
Common::FixedPoint<50, 14>::from_base(biquads[0].b[1]).to_double(),
|
||||
Common::FixedPoint<50, 14>::from_base(biquads[0].b[2]).to_double()};
|
||||
std::array<f64, 2> a0{Common::FixedPoint<50, 14>::from_base(biquads[0].a[0]).to_double(),
|
||||
Common::FixedPoint<50, 14>::from_base(biquads[0].a[1]).to_double()};
|
||||
|
||||
// Convert second filter coefficients
|
||||
std::array<f64, 3> b1{Common::FixedPoint<50, 14>::from_base(biquads[1].b[0]).to_double(),
|
||||
Common::FixedPoint<50, 14>::from_base(biquads[1].b[1]).to_double(),
|
||||
Common::FixedPoint<50, 14>::from_base(biquads[1].b[2]).to_double()};
|
||||
std::array<f64, 2> a1{Common::FixedPoint<50, 14>::from_base(biquads[1].a[0]).to_double(),
|
||||
Common::FixedPoint<50, 14>::from_base(biquads[1].a[1]).to_double()};
|
||||
|
||||
// Get states
|
||||
std::array<f64, 4> s0{Common::BitCast<f64>(states[0].s0), Common::BitCast<f64>(states[0].s1),
|
||||
Common::BitCast<f64>(states[0].s2), Common::BitCast<f64>(states[0].s3)};
|
||||
std::array<f64, 4> s1{Common::BitCast<f64>(states[1].s0), Common::BitCast<f64>(states[1].s1),
|
||||
Common::BitCast<f64>(states[1].s2), Common::BitCast<f64>(states[1].s3)};
|
||||
|
||||
for (u32 i = 0; i < sample_count; i++) {
|
||||
f64 in_sample{static_cast<f64>(input[i])};
|
||||
|
||||
// First filter
|
||||
auto filtered0{in_sample * b0[0] + s0[0] * b0[1] + s0[1] * b0[2] + s0[2] * a0[0] +
|
||||
s0[3] * a0[1]};
|
||||
|
||||
s0[1] = s0[0];
|
||||
s0[0] = in_sample;
|
||||
s0[3] = s0[2];
|
||||
s0[2] = filtered0;
|
||||
|
||||
// Second filter (uses output of first)
|
||||
auto filtered1{filtered0 * b1[0] + s1[0] * b1[1] + s1[1] * b1[2] + s1[2] * a1[0] +
|
||||
s1[3] * a1[1]};
|
||||
|
||||
s1[1] = s1[0];
|
||||
s1[0] = filtered0;
|
||||
s1[3] = s1[2];
|
||||
s1[2] = filtered1;
|
||||
|
||||
// Mix into output (additive)
|
||||
f64 mixed{static_cast<f64>(output[i]) + filtered1 * static_cast<f64>(volume)};
|
||||
output[i] = static_cast<s32>(std::clamp(mixed, min, max));
|
||||
}
|
||||
|
||||
// Save states back
|
||||
states[0].s0 = Common::BitCast<s64>(s0[0]);
|
||||
states[0].s1 = Common::BitCast<s64>(s0[1]);
|
||||
states[0].s2 = Common::BitCast<s64>(s0[2]);
|
||||
states[0].s3 = Common::BitCast<s64>(s0[3]);
|
||||
states[1].s0 = Common::BitCast<s64>(s1[0]);
|
||||
states[1].s1 = Common::BitCast<s64>(s1[1]);
|
||||
states[1].s2 = Common::BitCast<s64>(s1[2]);
|
||||
states[1].s3 = Common::BitCast<s64>(s1[3]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply double biquad filter and mix the result into the output buffer with volume ramp (REV12+).
|
||||
*
|
||||
* @param output - Output container to mix filtered samples into.
|
||||
* @param input - Input container for samples to be filtered.
|
||||
* @param biquads - Array of two biquad filter parameters.
|
||||
* @param states - Array of two biquad filter states.
|
||||
* @param sample_count - Number of samples to process.
|
||||
* @param volume - Initial mix volume.
|
||||
* @param ramp - Volume increment step per sample.
|
||||
* @return Last filtered sample value.
|
||||
*/
|
||||
f32 ApplyDoubleBiquadFilterAndMixRamp(std::span<s32> output, std::span<const s32> input,
|
||||
std::array<VoiceInfo::BiquadFilterParameter, 2>& biquads,
|
||||
std::array<VoiceState::BiquadFilterState, 2>& states,
|
||||
const u32 sample_count, f32 volume, f32 ramp) {
|
||||
constexpr f64 min{std::numeric_limits<s32>::min()};
|
||||
constexpr f64 max{std::numeric_limits<s32>::max()};
|
||||
|
||||
// Convert first filter coefficients
|
||||
std::array<f64, 3> b0{Common::FixedPoint<50, 14>::from_base(biquads[0].b[0]).to_double(),
|
||||
Common::FixedPoint<50, 14>::from_base(biquads[0].b[1]).to_double(),
|
||||
Common::FixedPoint<50, 14>::from_base(biquads[0].b[2]).to_double()};
|
||||
std::array<f64, 2> a0{Common::FixedPoint<50, 14>::from_base(biquads[0].a[0]).to_double(),
|
||||
Common::FixedPoint<50, 14>::from_base(biquads[0].a[1]).to_double()};
|
||||
|
||||
// Convert second filter coefficients
|
||||
std::array<f64, 3> b1{Common::FixedPoint<50, 14>::from_base(biquads[1].b[0]).to_double(),
|
||||
Common::FixedPoint<50, 14>::from_base(biquads[1].b[1]).to_double(),
|
||||
Common::FixedPoint<50, 14>::from_base(biquads[1].b[2]).to_double()};
|
||||
std::array<f64, 2> a1{Common::FixedPoint<50, 14>::from_base(biquads[1].a[0]).to_double(),
|
||||
Common::FixedPoint<50, 14>::from_base(biquads[1].a[1]).to_double()};
|
||||
|
||||
// Get states
|
||||
std::array<f64, 4> s0{Common::BitCast<f64>(states[0].s0), Common::BitCast<f64>(states[0].s1),
|
||||
Common::BitCast<f64>(states[0].s2), Common::BitCast<f64>(states[0].s3)};
|
||||
std::array<f64, 4> s1{Common::BitCast<f64>(states[1].s0), Common::BitCast<f64>(states[1].s1),
|
||||
Common::BitCast<f64>(states[1].s2), Common::BitCast<f64>(states[1].s3)};
|
||||
|
||||
f32 current_volume{volume};
|
||||
f32 last_mixed{0.0f};
|
||||
|
||||
for (u32 i = 0; i < sample_count; i++) {
|
||||
f64 in_sample{static_cast<f64>(input[i])};
|
||||
|
||||
// First filter
|
||||
auto filtered0{in_sample * b0[0] + s0[0] * b0[1] + s0[1] * b0[2] + s0[2] * a0[0] +
|
||||
s0[3] * a0[1]};
|
||||
|
||||
s0[1] = s0[0];
|
||||
s0[0] = in_sample;
|
||||
s0[3] = s0[2];
|
||||
s0[2] = filtered0;
|
||||
|
||||
// Second filter (uses output of first)
|
||||
auto filtered1{filtered0 * b1[0] + s1[0] * b1[1] + s1[1] * b1[2] + s1[2] * a1[0] +
|
||||
s1[3] * a1[1]};
|
||||
|
||||
s1[1] = s1[0];
|
||||
s1[0] = filtered0;
|
||||
s1[3] = s1[2];
|
||||
s1[2] = filtered1;
|
||||
|
||||
// Mix into output with current volume
|
||||
last_mixed = static_cast<f32>(filtered1 * static_cast<f64>(current_volume));
|
||||
f64 mixed{static_cast<f64>(output[i]) + static_cast<f64>(last_mixed)};
|
||||
output[i] = static_cast<s32>(std::clamp(mixed, min, max));
|
||||
|
||||
current_volume += ramp;
|
||||
}
|
||||
|
||||
// Save states back
|
||||
states[0].s0 = Common::BitCast<s64>(s0[0]);
|
||||
states[0].s1 = Common::BitCast<s64>(s0[1]);
|
||||
states[0].s2 = Common::BitCast<s64>(s0[2]);
|
||||
states[0].s3 = Common::BitCast<s64>(s0[3]);
|
||||
states[1].s0 = Common::BitCast<s64>(s1[0]);
|
||||
states[1].s1 = Common::BitCast<s64>(s1[1]);
|
||||
states[1].s2 = Common::BitCast<s64>(s1[2]);
|
||||
states[1].s3 = Common::BitCast<s64>(s1[3]);
|
||||
|
||||
return last_mixed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Biquad filter s32 implementation.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
@@ -90,4 +91,70 @@ void ApplyBiquadFilterFloat2(std::span<s32> output, std::span<const s32> input,
|
||||
std::array<f32, 3>& b, std::array<f32, 2>& a,
|
||||
VoiceState::BiquadFilterState& state, const u32 sample_count);
|
||||
|
||||
/**
|
||||
* Apply a single biquad filter and mix the result into the output buffer (REV12+).
|
||||
*
|
||||
* @param output - Output container to mix filtered samples into.
|
||||
* @param input - Input container for samples to be filtered.
|
||||
* @param b - Feedforward coefficients.
|
||||
* @param a - Feedback coefficients.
|
||||
* @param state - State to track previous samples.
|
||||
* @param sample_count - Number of samples to process.
|
||||
* @param volume - Mix volume.
|
||||
*/
|
||||
void ApplyBiquadFilterAndMix(std::span<s32> output, std::span<const s32> input,
|
||||
std::array<s16, 3>& b, std::array<s16, 2>& a,
|
||||
VoiceState::BiquadFilterState& state, const u32 sample_count,
|
||||
f32 volume);
|
||||
|
||||
/**
|
||||
* Apply a single biquad filter and mix the result into the output buffer with volume ramp (REV12+).
|
||||
*
|
||||
* @param output - Output container to mix filtered samples into.
|
||||
* @param input - Input container for samples to be filtered.
|
||||
* @param b - Feedforward coefficients.
|
||||
* @param a - Feedback coefficients.
|
||||
* @param state - State to track previous samples.
|
||||
* @param sample_count - Number of samples to process.
|
||||
* @param volume - Initial mix volume.
|
||||
* @param ramp - Volume increment step per sample.
|
||||
* @return Last filtered sample value.
|
||||
*/
|
||||
f32 ApplyBiquadFilterAndMixRamp(std::span<s32> output, std::span<const s32> input,
|
||||
std::array<s16, 3>& b, std::array<s16, 2>& a,
|
||||
VoiceState::BiquadFilterState& state, const u32 sample_count,
|
||||
f32 volume, f32 ramp);
|
||||
|
||||
/**
|
||||
* Apply double biquad filter and mix the result into the output buffer (REV12+).
|
||||
*
|
||||
* @param output - Output container to mix filtered samples into.
|
||||
* @param input - Input container for samples to be filtered.
|
||||
* @param biquads - Array of two biquad filter parameters.
|
||||
* @param states - Array of two biquad filter states.
|
||||
* @param sample_count - Number of samples to process.
|
||||
* @param volume - Mix volume.
|
||||
*/
|
||||
void ApplyDoubleBiquadFilterAndMix(std::span<s32> output, std::span<const s32> input,
|
||||
std::array<VoiceInfo::BiquadFilterParameter, 2>& biquads,
|
||||
std::array<VoiceState::BiquadFilterState, 2>& states,
|
||||
const u32 sample_count, f32 volume);
|
||||
|
||||
/**
|
||||
* Apply double biquad filter and mix the result into the output buffer with volume ramp (REV12+).
|
||||
*
|
||||
* @param output - Output container to mix filtered samples into.
|
||||
* @param input - Input container for samples to be filtered.
|
||||
* @param biquads - Array of two biquad filter parameters.
|
||||
* @param states - Array of two biquad filter states.
|
||||
* @param sample_count - Number of samples to process.
|
||||
* @param volume - Initial mix volume.
|
||||
* @param ramp - Volume increment step per sample.
|
||||
* @return Last filtered sample value.
|
||||
*/
|
||||
f32 ApplyDoubleBiquadFilterAndMixRamp(std::span<s32> output, std::span<const s32> input,
|
||||
std::array<VoiceInfo::BiquadFilterParameter, 2>& biquads,
|
||||
std::array<VoiceState::BiquadFilterState, 2>& states,
|
||||
const u32 sample_count, f32 volume, f32 ramp);
|
||||
|
||||
} // namespace AudioCore::Renderer
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "audio_core/adsp/apps/audio_renderer/command_list_processor.h"
|
||||
#include "audio_core/renderer/command/effect/biquad_filter.h"
|
||||
#include "audio_core/renderer/command/effect/biquad_filter_and_mix.h"
|
||||
#include "audio_core/renderer/voice/voice_state.h"
|
||||
|
||||
namespace AudioCore::Renderer {
|
||||
|
||||
void BiquadFilterAndMixCommand::Dump(
|
||||
[[maybe_unused]] const AudioRenderer::CommandListProcessor& processor, std::string& string) {
|
||||
string += fmt::format(
|
||||
"BiquadFilterAndMixCommand\n\tinput {:02X} output {:02X} needs_init {} "
|
||||
"has_volume_ramp {} is_first_mix_buffer {}\n",
|
||||
input, output, needs_init, has_volume_ramp, is_first_mix_buffer);
|
||||
}
|
||||
|
||||
void BiquadFilterAndMixCommand::Process(const AudioRenderer::CommandListProcessor& processor) {
|
||||
auto* state_{reinterpret_cast<VoiceState::BiquadFilterState*>(state)};
|
||||
auto* prev_state_{reinterpret_cast<VoiceState::BiquadFilterState*>(previous_state)};
|
||||
auto* voice_state_{reinterpret_cast<VoiceState*>(voice_state)};
|
||||
|
||||
if (needs_init) {
|
||||
*state_ = {};
|
||||
} else if (is_first_mix_buffer) {
|
||||
*prev_state_ = *state_;
|
||||
} else {
|
||||
*state_ = *prev_state_;
|
||||
}
|
||||
|
||||
auto input_buffer{
|
||||
processor.mix_buffers.subspan(input * processor.sample_count, processor.sample_count)};
|
||||
auto output_buffer{
|
||||
processor.mix_buffers.subspan(output * processor.sample_count, processor.sample_count)};
|
||||
|
||||
if (has_volume_ramp) {
|
||||
f32 ramp = (volume1 - volume0) / static_cast<f32>(processor.sample_count);
|
||||
f32 last_sample = ApplyBiquadFilterAndMixRamp(output_buffer, input_buffer, biquad.b,
|
||||
biquad.a, *state_, processor.sample_count,
|
||||
volume0, ramp);
|
||||
if (voice_state_ && last_sample_index >= 0 &&
|
||||
last_sample_index < static_cast<s32>(voice_state_->previous_samples.size())) {
|
||||
voice_state_->previous_samples[last_sample_index] = static_cast<s32>(last_sample);
|
||||
}
|
||||
} else {
|
||||
ApplyBiquadFilterAndMix(output_buffer, input_buffer, biquad.b, biquad.a, *state_,
|
||||
processor.sample_count, volume1);
|
||||
}
|
||||
}
|
||||
|
||||
bool BiquadFilterAndMixCommand::Verify(const AudioRenderer::CommandListProcessor& processor) {
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace AudioCore::Renderer
|
||||
@@ -0,0 +1,73 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "audio_core/renderer/command/icommand.h"
|
||||
#include "audio_core/renderer/voice/voice_info.h"
|
||||
#include "audio_core/renderer/voice/voice_state.h"
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace AudioCore::ADSP::AudioRenderer {
|
||||
class CommandListProcessor;
|
||||
}
|
||||
|
||||
namespace AudioCore::Renderer {
|
||||
|
||||
/**
|
||||
* AudioRenderer command for applying a biquad filter and mixing the result into the output buffer
|
||||
* (REV12+).
|
||||
*/
|
||||
struct BiquadFilterAndMixCommand : ICommand {
|
||||
/**
|
||||
* Print this command's information to a string.
|
||||
*
|
||||
* @param processor - The CommandListProcessor processing this command.
|
||||
* @param string - The string to print into.
|
||||
*/
|
||||
void Dump(const AudioRenderer::CommandListProcessor& processor, std::string& string) override;
|
||||
|
||||
/**
|
||||
* Process this command.
|
||||
*
|
||||
* @param processor - The CommandListProcessor processing this command.
|
||||
*/
|
||||
void Process(const AudioRenderer::CommandListProcessor& processor) override;
|
||||
|
||||
/**
|
||||
* Verify this command's data is valid.
|
||||
*
|
||||
* @param processor - The CommandListProcessor processing this command.
|
||||
* @return True if the command is valid, otherwise false.
|
||||
*/
|
||||
bool Verify(const AudioRenderer::CommandListProcessor& processor) override;
|
||||
|
||||
/// Input mix buffer index
|
||||
s16 input;
|
||||
/// Output mix buffer index
|
||||
s16 output;
|
||||
/// Input parameters for biquad (fixed-point)
|
||||
VoiceInfo::BiquadFilterParameter biquad;
|
||||
/// Biquad state, updated each call
|
||||
CpuAddr state;
|
||||
/// Previous biquad state (for state restoration)
|
||||
CpuAddr previous_state;
|
||||
/// Voice state address (for last sample storage)
|
||||
CpuAddr voice_state;
|
||||
/// Index in voice state last_samples array
|
||||
s32 last_sample_index;
|
||||
/// Initial volume (for ramp)
|
||||
f32 volume0;
|
||||
/// Final volume
|
||||
f32 volume1;
|
||||
/// If true, reset the state
|
||||
bool needs_init;
|
||||
/// If true, use volume ramp
|
||||
bool has_volume_ramp;
|
||||
/// If true, this is the first mix buffer
|
||||
bool is_first_mix_buffer;
|
||||
};
|
||||
|
||||
} // namespace AudioCore::Renderer
|
||||
@@ -0,0 +1,73 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "audio_core/adsp/apps/audio_renderer/command_list_processor.h"
|
||||
#include "audio_core/renderer/command/effect/biquad_filter.h"
|
||||
#include "audio_core/renderer/command/effect/multi_tap_biquad_filter_and_mix.h"
|
||||
#include "audio_core/renderer/voice/voice_state.h"
|
||||
|
||||
namespace AudioCore::Renderer {
|
||||
|
||||
void MultiTapBiquadFilterAndMixCommand::Dump(
|
||||
[[maybe_unused]] const AudioRenderer::CommandListProcessor& processor, std::string& string) {
|
||||
string += fmt::format(
|
||||
"MultiTapBiquadFilterAndMixCommand\n\tinput {:02X} output {:02X} "
|
||||
"has_volume_ramp {} is_first_mix_buffer {}\n",
|
||||
input, output, has_volume_ramp, is_first_mix_buffer);
|
||||
}
|
||||
|
||||
void MultiTapBiquadFilterAndMixCommand::Process(
|
||||
const AudioRenderer::CommandListProcessor& processor) {
|
||||
std::array<VoiceState::BiquadFilterState*, MaxBiquadFilters> states_{};
|
||||
std::array<VoiceState::BiquadFilterState*, MaxBiquadFilters> prev_states_{};
|
||||
auto* voice_state_{reinterpret_cast<VoiceState*>(voice_state)};
|
||||
|
||||
for (u32 i = 0; i < MaxBiquadFilters; i++) {
|
||||
states_[i] = reinterpret_cast<VoiceState::BiquadFilterState*>(states[i]);
|
||||
prev_states_[i] = reinterpret_cast<VoiceState::BiquadFilterState*>(previous_states[i]);
|
||||
|
||||
if (needs_init[i]) {
|
||||
*states_[i] = {};
|
||||
} else if (is_first_mix_buffer) {
|
||||
*prev_states_[i] = *states_[i];
|
||||
} else {
|
||||
*states_[i] = *prev_states_[i];
|
||||
}
|
||||
}
|
||||
|
||||
auto input_buffer{
|
||||
processor.mix_buffers.subspan(input * processor.sample_count, processor.sample_count)};
|
||||
auto output_buffer{
|
||||
processor.mix_buffers.subspan(output * processor.sample_count, processor.sample_count)};
|
||||
|
||||
std::array<VoiceState::BiquadFilterState, MaxBiquadFilters> states_array{};
|
||||
for (u32 i = 0; i < MaxBiquadFilters; i++) {
|
||||
states_array[i] = *states_[i];
|
||||
}
|
||||
|
||||
if (has_volume_ramp) {
|
||||
f32 ramp = (volume1 - volume0) / static_cast<f32>(processor.sample_count);
|
||||
f32 last_sample = ApplyDoubleBiquadFilterAndMixRamp(
|
||||
output_buffer, input_buffer, biquads, states_array, processor.sample_count, volume0,
|
||||
ramp);
|
||||
if (voice_state_ && last_sample_index >= 0 &&
|
||||
last_sample_index < static_cast<s32>(voice_state_->previous_samples.size())) {
|
||||
voice_state_->previous_samples[last_sample_index] = static_cast<s32>(last_sample);
|
||||
}
|
||||
} else {
|
||||
ApplyDoubleBiquadFilterAndMix(output_buffer, input_buffer, biquads, states_array,
|
||||
processor.sample_count, volume1);
|
||||
}
|
||||
|
||||
// Save states back
|
||||
for (u32 i = 0; i < MaxBiquadFilters; i++) {
|
||||
*states_[i] = states_array[i];
|
||||
}
|
||||
}
|
||||
|
||||
bool MultiTapBiquadFilterAndMixCommand::Verify(
|
||||
const AudioRenderer::CommandListProcessor& processor) {
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace AudioCore::Renderer
|
||||
@@ -0,0 +1,74 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
|
||||
#include "audio_core/renderer/command/icommand.h"
|
||||
#include "audio_core/renderer/voice/voice_info.h"
|
||||
#include "audio_core/renderer/voice/voice_state.h"
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace AudioCore::ADSP::AudioRenderer {
|
||||
class CommandListProcessor;
|
||||
}
|
||||
|
||||
namespace AudioCore::Renderer {
|
||||
|
||||
/**
|
||||
* AudioRenderer command for applying two biquad filters and mixing the result into the output buffer
|
||||
* (REV12+).
|
||||
*/
|
||||
struct MultiTapBiquadFilterAndMixCommand : ICommand {
|
||||
/**
|
||||
* Print this command's information to a string.
|
||||
*
|
||||
* @param processor - The CommandListProcessor processing this command.
|
||||
* @param string - The string to print into.
|
||||
*/
|
||||
void Dump(const AudioRenderer::CommandListProcessor& processor, std::string& string) override;
|
||||
|
||||
/**
|
||||
* Process this command.
|
||||
*
|
||||
* @param processor - The CommandListProcessor processing this command.
|
||||
*/
|
||||
void Process(const AudioRenderer::CommandListProcessor& processor) override;
|
||||
|
||||
/**
|
||||
* Verify this command's data is valid.
|
||||
*
|
||||
* @param processor - The CommandListProcessor processing this command.
|
||||
* @return True if the command is valid, otherwise false.
|
||||
*/
|
||||
bool Verify(const AudioRenderer::CommandListProcessor& processor) override;
|
||||
|
||||
/// Input mix buffer index
|
||||
s16 input;
|
||||
/// Output mix buffer index
|
||||
s16 output;
|
||||
/// Input parameters for biquads (fixed-point)
|
||||
std::array<VoiceInfo::BiquadFilterParameter, MaxBiquadFilters> biquads;
|
||||
/// Biquad states, updated each call
|
||||
std::array<CpuAddr, MaxBiquadFilters> states;
|
||||
/// Previous biquad states (for state restoration)
|
||||
std::array<CpuAddr, MaxBiquadFilters> previous_states;
|
||||
/// Voice state address (for last sample storage)
|
||||
CpuAddr voice_state;
|
||||
/// Index in voice state last_samples array
|
||||
s32 last_sample_index;
|
||||
/// Initial volume (for ramp)
|
||||
f32 volume0;
|
||||
/// Final volume
|
||||
f32 volume1;
|
||||
/// If each biquad needs initialisation
|
||||
std::array<bool, MaxBiquadFilters> needs_init;
|
||||
/// If true, use volume ramp
|
||||
bool has_volume_ramp;
|
||||
/// If true, this is the first mix buffer
|
||||
bool is_first_mix_buffer;
|
||||
};
|
||||
|
||||
} // namespace AudioCore::Renderer
|
||||
@@ -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
|
||||
@@ -47,6 +48,8 @@ enum class CommandId : u8 {
|
||||
/* 0x1C */ MultiTapBiquadFilter,
|
||||
/* 0x1D */ Capture,
|
||||
/* 0x1E */ Compressor,
|
||||
/* 0x1F */ BiquadFilterAndMix,
|
||||
/* 0x20 */ MultiTapBiquadFilterAndMix,
|
||||
};
|
||||
|
||||
constexpr u32 CommandMagic{0xCAFEBABE};
|
||||
|
||||
@@ -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
|
||||
|
||||
#include "audio_core/common/audio_renderer_parameter.h"
|
||||
@@ -41,6 +42,7 @@ void SplitterContext::Setup(std::span<SplitterInfo> splitter_infos_, const u32 s
|
||||
splitter_bug_fixed = splitter_bug_fixed_;
|
||||
splitter_prev_volume_reset_supported = behavior.IsSplitterPrevVolumeResetSupported();
|
||||
splitter_float_coeff_supported = behavior.IsSplitterDestinationV2bSupported();
|
||||
splitter_biquad_filter_enabled = behavior.IsBiquadFilterParameterForSplitterEnabled();
|
||||
}
|
||||
|
||||
bool SplitterContext::UsingSplitter() const {
|
||||
@@ -56,7 +58,8 @@ void SplitterContext::ClearAllNewConnectionFlag() {
|
||||
|
||||
bool SplitterContext::Initialize(const BehaviorInfo& behavior,
|
||||
const AudioRendererParameterInternal& params,
|
||||
WorkbufferAllocator& allocator) {
|
||||
WorkbufferAllocator& allocator,
|
||||
std::span<VoiceState::BiquadFilterState> splitter_bqf_states_) {
|
||||
if (behavior.IsSplitterSupported() && params.splitter_infos > 0 &&
|
||||
params.splitter_destinations > 0) {
|
||||
splitter_infos = allocator.Allocate<SplitterInfo>(params.splitter_infos, 0x10);
|
||||
@@ -83,6 +86,12 @@ bool SplitterContext::Initialize(const BehaviorInfo& behavior,
|
||||
return false;
|
||||
}
|
||||
|
||||
// Store biquad filter states (REV12+)
|
||||
if (behavior.IsBiquadFilterParameterForSplitterEnabled() &&
|
||||
!splitter_bqf_states_.empty()) {
|
||||
splitter_bqf_states = splitter_bqf_states_;
|
||||
}
|
||||
|
||||
Setup(splitter_infos, params.splitter_infos, splitter_destinations,
|
||||
params.splitter_destinations, behavior.IsSplitterBugFixed(), behavior);
|
||||
}
|
||||
@@ -90,13 +99,15 @@ bool SplitterContext::Initialize(const BehaviorInfo& behavior,
|
||||
}
|
||||
|
||||
bool SplitterContext::Update(const u8* input, u32& consumed_size) {
|
||||
auto in_params{reinterpret_cast<const InParameterHeader*>(input)};
|
||||
|
||||
if (destinations_count == 0 || info_count == 0) {
|
||||
// If no splitters are configured, skip splitter update
|
||||
if (!UsingSplitter()) {
|
||||
consumed_size = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
auto in_params{reinterpret_cast<const InParameterHeader*>(input)};
|
||||
|
||||
// Check if header magic matches - if not, this is an error
|
||||
if (in_params->magic != GetSplitterInParamHeaderMagic()) {
|
||||
consumed_size = 0;
|
||||
return false;
|
||||
@@ -136,7 +147,61 @@ u32 SplitterContext::UpdateInfo(const u8* input, u32 offset, const u32 splitter_
|
||||
}
|
||||
|
||||
u32 SplitterContext::UpdateData(const u8* input, u32 offset, const u32 count) {
|
||||
LOG_DEBUG(Service_Audio,
|
||||
"UpdateData: count={}, REV12_enabled={}, float_coeff_supported={}, "
|
||||
"prev_volume_reset_supported={}",
|
||||
count, splitter_biquad_filter_enabled, splitter_float_coeff_supported,
|
||||
splitter_prev_volume_reset_supported);
|
||||
|
||||
for (u32 i = 0; i < count; i++) {
|
||||
// REV12: Use InParameterVersion2a (fixed-point biquad filters)
|
||||
if (splitter_biquad_filter_enabled && !splitter_float_coeff_supported) {
|
||||
const auto* data_header_v2a =
|
||||
reinterpret_cast<const SplitterDestinationData::InParameterVersion2a*>(input + offset);
|
||||
|
||||
if (data_header_v2a->magic != GetSplitterSendDataMagic()) {
|
||||
// Skip invalid entries but still advance by REV12 size
|
||||
offset += sizeof(SplitterDestinationData::InParameterVersion2a);
|
||||
continue;
|
||||
}
|
||||
if (data_header_v2a->id < 0 || data_header_v2a->id >= static_cast<s32>(destinations_count)) {
|
||||
// Skip invalid entries but still advance by REV12 size
|
||||
offset += sizeof(SplitterDestinationData::InParameterVersion2a);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Map common fields to the base format for REV13+ handling
|
||||
SplitterDestinationData::InParameter mapped{};
|
||||
mapped.magic = data_header_v2a->magic;
|
||||
mapped.id = data_header_v2a->id;
|
||||
mapped.mix_volumes = data_header_v2a->mix_volumes;
|
||||
mapped.mix_id = data_header_v2a->mix_id;
|
||||
mapped.in_use = data_header_v2a->in_use;
|
||||
mapped.reset_prev_volume =
|
||||
splitter_prev_volume_reset_supported ? data_header_v2a->reset_prev_volume : false;
|
||||
|
||||
auto& destination = splitter_destinations[data_header_v2a->id];
|
||||
destination.Update(mapped, splitter_prev_volume_reset_supported);
|
||||
|
||||
// Convert legacy fixed-point biquad params into float representation (for REV15+ compatibility)
|
||||
auto biquad_filters = destination.GetBiquadFilters();
|
||||
for (size_t filter_idx = 0; filter_idx < MaxBiquadFilters; filter_idx++) {
|
||||
const auto& legacy = data_header_v2a->biquad_filters[filter_idx];
|
||||
auto& out = biquad_filters[filter_idx];
|
||||
out.enabled = legacy.enabled;
|
||||
// s16 fixed-point scale: use Q14 like voices (b and a are s16, 1.0 ~= 1<<14)
|
||||
constexpr float scale = 1.0f / static_cast<float>(1 << 14);
|
||||
out.numerator[0] = static_cast<float>(legacy.b[0]) * scale;
|
||||
out.numerator[1] = static_cast<float>(legacy.b[1]) * scale;
|
||||
out.numerator[2] = static_cast<float>(legacy.b[2]) * scale;
|
||||
out.denominator[0] = static_cast<float>(legacy.a[0]) * scale;
|
||||
out.denominator[1] = static_cast<float>(legacy.a[1]) * scale;
|
||||
}
|
||||
|
||||
offset += sizeof(SplitterDestinationData::InParameterVersion2a);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Version selection based on float coeff/biquad v2b support.
|
||||
if (!splitter_float_coeff_supported) {
|
||||
const auto* data_header =
|
||||
@@ -149,11 +214,13 @@ u32 SplitterContext::UpdateData(const u8* input, u32 offset, const u32 count) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// REV13+: Modify params to clear reset_prev_volume if not supported
|
||||
auto modified_params = *data_header;
|
||||
if (!splitter_prev_volume_reset_supported) {
|
||||
modified_params.reset_prev_volume = false;
|
||||
}
|
||||
splitter_destinations[data_header->id].Update(modified_params);
|
||||
splitter_destinations[data_header->id].Update(modified_params,
|
||||
splitter_prev_volume_reset_supported);
|
||||
offset += sizeof(SplitterDestinationData::InParameter);
|
||||
} else {
|
||||
// Version 2b: struct contains extra biquad filter fields
|
||||
@@ -167,18 +234,19 @@ u32 SplitterContext::UpdateData(const u8* input, u32 offset, const u32 count) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Map common fields to the old format
|
||||
// Map common fields to the base format for REV13+ handling
|
||||
SplitterDestinationData::InParameter mapped{};
|
||||
mapped.magic = data_header_v2b->magic;
|
||||
mapped.id = data_header_v2b->id;
|
||||
mapped.mix_volumes = data_header_v2b->mix_volumes;
|
||||
mapped.mix_id = data_header_v2b->mix_id;
|
||||
mapped.in_use = data_header_v2b->in_use;
|
||||
mapped.reset_prev_volume = splitter_prev_volume_reset_supported ? data_header_v2b->reset_prev_volume : false;
|
||||
mapped.reset_prev_volume =
|
||||
splitter_prev_volume_reset_supported ? data_header_v2b->reset_prev_volume : false;
|
||||
|
||||
// Store biquad filters from V2b (REV15+)
|
||||
auto& destination = splitter_destinations[data_header_v2b->id];
|
||||
destination.Update(mapped);
|
||||
destination.Update(mapped, splitter_prev_volume_reset_supported);
|
||||
|
||||
// Copy biquad filter parameters
|
||||
auto biquad_filters = destination.GetBiquadFilters();
|
||||
@@ -239,6 +307,15 @@ u32 SplitterContext::GetDestCountPerInfoForCompat() const {
|
||||
return static_cast<u32>(destinations_count / info_count);
|
||||
}
|
||||
|
||||
std::span<VoiceState::BiquadFilterState> SplitterContext::GetBiquadFilterState(s32 destination_id) {
|
||||
if (splitter_bqf_states.empty() || destination_id < 0 ||
|
||||
destination_id >= static_cast<s32>(splitter_bqf_states.size() / BqfStatesPerDestination)) {
|
||||
return {};
|
||||
}
|
||||
return splitter_bqf_states.subspan(destination_id * BqfStatesPerDestination,
|
||||
BqfStatesPerDestination);
|
||||
}
|
||||
|
||||
u64 SplitterContext::CalcWorkBufferSize(const BehaviorInfo& behavior,
|
||||
const AudioRendererParameterInternal& params) {
|
||||
u64 size{0};
|
||||
@@ -252,6 +329,14 @@ u64 SplitterContext::CalcWorkBufferSize(const BehaviorInfo& behavior,
|
||||
if (behavior.IsSplitterBugFixed()) {
|
||||
size += Common::AlignUp(params.splitter_destinations * sizeof(u32), 0x10);
|
||||
}
|
||||
|
||||
// REV12+: Biquad filter states for splitters
|
||||
if (behavior.IsBiquadFilterParameterForSplitterEnabled() &&
|
||||
params.splitter_destinations > 0) {
|
||||
size = Common::AlignUp(size, 0x10);
|
||||
size += params.splitter_destinations * BqfStatesPerDestination *
|
||||
sizeof(VoiceState::BiquadFilterState);
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -7,6 +8,7 @@
|
||||
|
||||
#include "audio_core/renderer/splitter/splitter_destinations_data.h"
|
||||
#include "audio_core/renderer/splitter/splitter_info.h"
|
||||
#include "audio_core/renderer/voice/voice_state.h"
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace AudioCore {
|
||||
@@ -35,6 +37,10 @@ class SplitterContext {
|
||||
"SplitterContext::InParameterHeader has the wrong size!");
|
||||
|
||||
public:
|
||||
/**
|
||||
* Amount of biquad filter states per splitter destination (REV12+).
|
||||
*/
|
||||
static constexpr u32 BqfStatesPerDestination = 4;
|
||||
/**
|
||||
* Get a destination mix from the given splitter and destination index.
|
||||
*
|
||||
@@ -92,9 +98,11 @@ public:
|
||||
* @param behavior - Used to check for splitter support.
|
||||
* @param params - Input parameters.
|
||||
* @param allocator - Allocator used to allocate workbuffer memory.
|
||||
* @param splitter_bqf_states - Memory span for biquad filter states (REV12+).
|
||||
*/
|
||||
bool Initialize(const BehaviorInfo& behavior, const AudioRendererParameterInternal& params,
|
||||
WorkbufferAllocator& allocator);
|
||||
WorkbufferAllocator& allocator,
|
||||
std::span<VoiceState::BiquadFilterState> splitter_bqf_states = {});
|
||||
|
||||
/**
|
||||
* Update the context.
|
||||
@@ -149,6 +157,14 @@ public:
|
||||
*/
|
||||
u32 GetDestCountPerInfoForCompat() const;
|
||||
|
||||
/**
|
||||
* Get biquad filter state for a specific destination (REV12+).
|
||||
*
|
||||
* @param destination_id - Destination index.
|
||||
* @return Span of biquad filter states for this destination.
|
||||
*/
|
||||
std::span<VoiceState::BiquadFilterState> GetBiquadFilterState(s32 destination_id);
|
||||
|
||||
/**
|
||||
* Calculate the size of the required workbuffer for splitters and destinations.
|
||||
*
|
||||
@@ -188,6 +204,10 @@ private:
|
||||
bool splitter_prev_volume_reset_supported{};
|
||||
/// Is float coefficient/biquad filter v2b parameter supported?
|
||||
bool splitter_float_coeff_supported{};
|
||||
/// Is biquad filter parameter for splitter enabled (REV12+)?
|
||||
bool splitter_biquad_filter_enabled{};
|
||||
/// Splitter biquad filtering states (REV12+)
|
||||
std::span<VoiceState::BiquadFilterState> splitter_bqf_states{};
|
||||
};
|
||||
|
||||
} // namespace Renderer
|
||||
|
||||
@@ -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
|
||||
|
||||
#include "audio_core/renderer/splitter/splitter_destinations_data.h"
|
||||
@@ -49,7 +50,8 @@ std::span<f32> SplitterDestinationData::GetMixVolumePrev() {
|
||||
return prev_mix_volumes;
|
||||
}
|
||||
|
||||
void SplitterDestinationData::Update(const InParameter& params) {
|
||||
void SplitterDestinationData::Update(const InParameter& params,
|
||||
bool is_prev_volume_reset_supported) {
|
||||
if (params.id != id || params.magic != GetSplitterSendDataMagic()) {
|
||||
return;
|
||||
}
|
||||
@@ -57,10 +59,31 @@ void SplitterDestinationData::Update(const InParameter& params) {
|
||||
destination_id = params.mix_id;
|
||||
mix_volumes = params.mix_volumes;
|
||||
|
||||
if (params.reset_prev_volume) {
|
||||
// REV13+: Use explicit reset flag if supported, otherwise use implicit reset on first use
|
||||
bool reset_prev_volume = is_prev_volume_reset_supported ? params.reset_prev_volume
|
||||
: (!in_use && params.in_use);
|
||||
if (reset_prev_volume) {
|
||||
prev_mix_volumes = mix_volumes;
|
||||
need_update = false;
|
||||
} else if (!in_use && params.in_use) {
|
||||
}
|
||||
|
||||
in_use = params.in_use;
|
||||
}
|
||||
|
||||
void SplitterDestinationData::Update(const InParameterVersion2a& params,
|
||||
bool is_prev_volume_reset_supported) {
|
||||
if (params.id != id || params.magic != GetSplitterSendDataMagic()) {
|
||||
return;
|
||||
}
|
||||
|
||||
destination_id = params.mix_id;
|
||||
mix_volumes = params.mix_volumes;
|
||||
biquad_filters_rev12 = params.biquad_filters; // REV12 addition
|
||||
|
||||
// REV13+: Use explicit reset flag if supported, otherwise use implicit reset on first use
|
||||
bool reset_prev_volume = is_prev_volume_reset_supported ? params.reset_prev_volume
|
||||
: (!in_use && params.in_use);
|
||||
if (reset_prev_volume) {
|
||||
prev_mix_volumes = mix_volumes;
|
||||
need_update = false;
|
||||
}
|
||||
@@ -95,4 +118,12 @@ std::span<const SplitterDestinationData::BiquadFilterParameter2> SplitterDestina
|
||||
return biquad_filters;
|
||||
}
|
||||
|
||||
std::span<VoiceInfo::BiquadFilterParameter> SplitterDestinationData::GetBiquadFiltersRev12() {
|
||||
return biquad_filters_rev12;
|
||||
}
|
||||
|
||||
std::span<const VoiceInfo::BiquadFilterParameter> SplitterDestinationData::GetBiquadFiltersRev12() const {
|
||||
return biquad_filters_rev12;
|
||||
}
|
||||
|
||||
} // namespace AudioCore::Renderer
|
||||
|
||||
@@ -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
|
||||
@@ -7,13 +8,11 @@
|
||||
#include <span>
|
||||
|
||||
#include "audio_core/common/common.h"
|
||||
#include "audio_core/renderer/voice/voice_info.h"
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace AudioCore::Renderer {
|
||||
|
||||
// Forward declaration
|
||||
class VoiceInfo;
|
||||
|
||||
/**
|
||||
* Represents a mixing node, can be connected to a previous and next destination forming a chain
|
||||
* that a certain mix buffer will pass through to output.
|
||||
@@ -46,6 +45,19 @@ public:
|
||||
static_assert(sizeof(InParameter) == 0x70,
|
||||
"SplitterDestinationData::InParameter has the wrong size!");
|
||||
|
||||
struct InParameterVersion2a {
|
||||
/* 0x00 */ u32 magic; // 'SNDD'
|
||||
/* 0x04 */ s32 id;
|
||||
/* 0x08 */ std::array<f32, MaxMixBuffers> mix_volumes;
|
||||
/* 0x68 */ u32 mix_id;
|
||||
/* 0x6C */ std::array<VoiceInfo::BiquadFilterParameter, MaxBiquadFilters> biquad_filters;
|
||||
/* 0x84 */ bool in_use;
|
||||
/* 0x85 */ bool reset_prev_volume;
|
||||
/* 0x86 */ u8 reserved[10];
|
||||
};
|
||||
static_assert(sizeof(InParameterVersion2a) == 0x90,
|
||||
"SplitterDestinationData::InParameterVersion2a has the wrong size!");
|
||||
|
||||
struct InParameterVersion2b {
|
||||
/* 0x00 */ u32 magic; // 'SNDD'
|
||||
/* 0x04 */ s32 id;
|
||||
@@ -121,8 +133,17 @@ public:
|
||||
* Update this destination.
|
||||
*
|
||||
* @param params - Input parameters to update the destination.
|
||||
* @param is_prev_volume_reset_supported - If true, use explicit reset flag; otherwise use implicit reset on first use.
|
||||
*/
|
||||
void Update(const InParameter& params);
|
||||
void Update(const InParameter& params, bool is_prev_volume_reset_supported = false);
|
||||
|
||||
/**
|
||||
* Update this destination (REV12).
|
||||
*
|
||||
* @param params - Input parameters to update the destination.
|
||||
* @param is_prev_volume_reset_supported - If true, use explicit reset flag; otherwise use implicit reset on first use.
|
||||
*/
|
||||
void Update(const InParameterVersion2a& params, bool is_prev_volume_reset_supported = false);
|
||||
|
||||
/**
|
||||
* Mark this destination as needing its volumes updated.
|
||||
@@ -162,6 +183,20 @@ public:
|
||||
*/
|
||||
std::span<const BiquadFilterParameter2> GetBiquadFilters() const;
|
||||
|
||||
/**
|
||||
* Get biquad filter parameters for this destination (REV12).
|
||||
*
|
||||
* @return Span of biquad filter parameters.
|
||||
*/
|
||||
std::span<VoiceInfo::BiquadFilterParameter> GetBiquadFiltersRev12();
|
||||
|
||||
/**
|
||||
* Get const biquad filter parameters for this destination (REV12).
|
||||
*
|
||||
* @return Const span of biquad filter parameters.
|
||||
*/
|
||||
std::span<const VoiceInfo::BiquadFilterParameter> GetBiquadFiltersRev12() const;
|
||||
|
||||
private:
|
||||
/// Id of this destination
|
||||
const s32 id;
|
||||
@@ -171,7 +206,9 @@ private:
|
||||
std::array<f32, MaxMixBuffers> mix_volumes{0.0f};
|
||||
/// Previous mix volumes
|
||||
std::array<f32, MaxMixBuffers> prev_mix_volumes{0.0f};
|
||||
/// Biquad filter parameters (REV15+)
|
||||
/// Biquad filter parameters (REV12, fixed-point)
|
||||
std::array<VoiceInfo::BiquadFilterParameter, MaxBiquadFilters> biquad_filters_rev12{};
|
||||
/// Biquad filter parameters (REV15+, float)
|
||||
std::array<BiquadFilterParameter2, MaxBiquadFilters> biquad_filters{};
|
||||
/// Next destination in the mix chain
|
||||
SplitterDestinationData* next{};
|
||||
|
||||
@@ -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
|
||||
|
||||
#include <chrono>
|
||||
@@ -118,6 +119,14 @@ Result System::Initialize(const AudioRendererParameterInternal& params,
|
||||
|
||||
behavior.SetUserLibRevision(params.revision);
|
||||
|
||||
LOG_INFO(Service_Audio, "Initializing audio renderer with revision {} (numeric: {})",
|
||||
::AudioCore::GetRevisionNum(params.revision), params.revision);
|
||||
LOG_DEBUG(Service_Audio,
|
||||
"Audio renderer params: voices={}, mixes={}, effects={}, sinks={}, "
|
||||
"splitter_infos={}, splitter_destinations={}",
|
||||
params.voices, params.mixes, params.effects, params.sinks,
|
||||
params.splitter_infos, params.splitter_destinations);
|
||||
|
||||
process_handle = process_handle_;
|
||||
applet_resource_user_id = applet_resource_user_id_;
|
||||
session_id = session_id_;
|
||||
@@ -266,9 +275,23 @@ Result System::Initialize(const AudioRendererParameterInternal& params,
|
||||
return Service::Audio::ResultInsufficientBuffer;
|
||||
}
|
||||
|
||||
if (!splitter_context.Initialize(behavior, params, allocator)) {
|
||||
// Allocate biquad filter states for splitters (REV12+)
|
||||
std::span<VoiceState::BiquadFilterState> splitter_bqf_states{};
|
||||
if (behavior.IsBiquadFilterParameterForSplitterEnabled() &&
|
||||
params.splitter_destinations > 0) {
|
||||
splitter_bqf_states = allocator.Allocate<VoiceState::BiquadFilterState>(
|
||||
params.splitter_destinations * SplitterContext::BqfStatesPerDestination, 0x10);
|
||||
if (splitter_bqf_states.empty()) {
|
||||
return Service::Audio::ResultInsufficientBuffer;
|
||||
}
|
||||
std::memset(splitter_bqf_states.data(), 0, splitter_bqf_states.size_bytes());
|
||||
}
|
||||
|
||||
if (!splitter_context.Initialize(behavior, params, allocator, splitter_bqf_states)) {
|
||||
LOG_ERROR(Service_Audio, "Failed to initialize splitter context!");
|
||||
return Service::Audio::ResultInsufficientBuffer;
|
||||
}
|
||||
LOG_DEBUG(Service_Audio, "Splitter context initialized successfully");
|
||||
|
||||
std::span<EffectResultState> effect_result_states_cpu{};
|
||||
if (behavior.IsEffectInfoVersion2Supported() && params.effects > 0) {
|
||||
@@ -451,6 +474,9 @@ Result System::Update(std::span<const u8> input, std::span<u8> performance, std:
|
||||
const auto start_time{core.CoreTiming().GetGlobalTimeNs().count()};
|
||||
std::memset(output.data(), 0, output.size());
|
||||
|
||||
LOG_DEBUG(Service_Audio, "Audio renderer update - user revision: {} (numeric: {})",
|
||||
behavior.GetUserRevision(), behavior.GetUserRevisionNum());
|
||||
|
||||
InfoUpdater info_updater(input, output, process_handle, behavior);
|
||||
|
||||
auto result{info_updater.UpdateBehaviorInfo(behavior)};
|
||||
|
||||
Reference in New Issue
Block a user