From f1dce2578ead9c967667d6b51e11d172a56fb693 Mon Sep 17 00:00:00 2001 From: Zephyron Date: Sat, 3 Jan 2026 15:14:44 +1000 Subject: [PATCH] audio_core: Add Audio Renderer REV12 and REV13 support Implements support for Audio Renderer revisions 12 and 13, adding: - REV12: Splitter biquad filter support with fixed-point coefficients - REV13: Explicit splitter previous volume reset and compressor statistics Signed-off-by: Zephyron --- src/audio_core/CMakeLists.txt | 4 + src/audio_core/common/feature_support.h | 2 + .../renderer/behavior/behavior_info.cpp | 5 + .../renderer/behavior/behavior_info.h | 9 + .../renderer/behavior/info_updater.cpp | 46 +++- .../renderer/command/command_buffer.cpp | 56 ++++ .../renderer/command/command_buffer.h | 51 ++++ .../renderer/command/command_generator.cpp | 106 ++++++- .../command_processing_time_estimator.cpp | 55 ++++ .../command_processing_time_estimator.h | 13 + src/audio_core/renderer/command/commands.h | 3 + .../renderer/command/effect/biquad_filter.cpp | 259 ++++++++++++++++++ .../renderer/command/effect/biquad_filter.h | 67 +++++ .../command/effect/biquad_filter_and_mix.cpp | 56 ++++ .../command/effect/biquad_filter_and_mix.h | 73 +++++ .../multi_tap_biquad_filter_and_mix.cpp | 73 +++++ .../effect/multi_tap_biquad_filter_and_mix.h | 74 +++++ src/audio_core/renderer/command/icommand.h | 3 + .../renderer/splitter/splitter_context.cpp | 101 ++++++- .../renderer/splitter/splitter_context.h | 22 +- .../splitter/splitter_destinations_data.cpp | 37 ++- .../splitter/splitter_destinations_data.h | 47 +++- src/audio_core/renderer/system.cpp | 28 +- 23 files changed, 1163 insertions(+), 27 deletions(-) create mode 100644 src/audio_core/renderer/command/effect/biquad_filter_and_mix.cpp create mode 100644 src/audio_core/renderer/command/effect/biquad_filter_and_mix.h create mode 100644 src/audio_core/renderer/command/effect/multi_tap_biquad_filter_and_mix.cpp create mode 100644 src/audio_core/renderer/command/effect/multi_tap_biquad_filter_and_mix.h diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt index bc15cae88..e0c1d4261 100644 --- a/src/audio_core/CMakeLists.txt +++ b/src/audio_core/CMakeLists.txt @@ -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 diff --git a/src/audio_core/common/feature_support.h b/src/audio_core/common/feature_support.h index e1182a918..79250ae47 100644 --- a/src/audio_core/common/feature_support.h +++ b/src/audio_core/common/feature_support.h @@ -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}, }}; diff --git a/src/audio_core/renderer/behavior/behavior_info.cpp b/src/audio_core/renderer/behavior/behavior_info.cpp index b046b7d81..68f5fa210 100644 --- a/src/audio_core/renderer/behavior/behavior_info.cpp +++ b/src/audio_core/renderer/behavior/behavior_info.cpp @@ -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 diff --git a/src/audio_core/renderer/behavior/behavior_info.h b/src/audio_core/renderer/behavior/behavior_info.h index c3d0f914b..6b4c1e470 100644 --- a/src/audio_core/renderer/behavior/behavior_info.h +++ b/src/audio_core/renderer/behavior/behavior_info.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 #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 diff --git a/src/audio_core/renderer/behavior/info_updater.cpp b/src/audio_core/renderer/behavior/info_updater.cpp index e8467a9bc..2986b1bed 100644 --- a/src/audio_core/renderer/behavior/info_updater.cpp +++ b/src/audio_core/renderer/behavior/info_updater.cpp @@ -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(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; diff --git a/src/audio_core/renderer/command/command_buffer.cpp b/src/audio_core/renderer/command/command_buffer.cpp index 0cb53e24f..026832c57 100644 --- a/src/audio_core/renderer/command/command_buffer.cpp +++ b/src/audio_core/renderer/command/command_buffer.cpp @@ -363,6 +363,62 @@ void CommandBuffer::GenerateMixRampGroupedCommand(const s32 node_id, const s16 b GenerateEnd(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(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(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& filters, + const std::array& biquad_states, + const std::array& previous_biquad_states, + const std::array& needs_init, bool has_volume_ramp, + bool is_first_mix_buffer) { + auto& cmd{GenerateStart(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(cmd); +} + void CommandBuffer::GenerateDepopPrepareCommand(const s32 node_id, const VoiceState& voice_state, std::span buffer, const s16 buffer_count, s16 buffer_offset, const bool was_playing) { diff --git a/src/audio_core/renderer/command/command_buffer.h b/src/audio_core/renderer/command/command_buffer.h index 12e8c2c81..411ca2e93 100644 --- a/src/audio_core/renderer/command/command_buffer.h +++ b/src/audio_core/renderer/command/command_buffer.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 #pragma once @@ -217,6 +218,56 @@ public: std::span 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& filters, + const std::array& biquad_states, + const std::array& previous_biquad_states, + const std::array& needs_init, bool has_volume_ramp, + bool is_first_mix_buffer); + /** * Generate a depop prepare command, adding it to the command list. * diff --git a/src/audio_core/renderer/command/command_generator.cpp b/src/audio_core/renderer/command/command_generator.cpp index 854eb3700..8196b2ee2 100644 --- a/src/audio_core/renderer/command/command_generator.cpp +++ b/src/audio_core/renderer/command/command_generator.cpp @@ -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(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 filters{}; + std::array states{}; + std::array prev_states{}; + std::array 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); + } } } } diff --git a/src/audio_core/renderer/command/command_processing_time_estimator.cpp b/src/audio_core/renderer/command/command_processing_time_estimator.cpp index 0f7aff1b4..2cfb6eade 100644 --- a/src/audio_core/renderer/command/command_processing_time_estimator.cpp +++ b/src/audio_core/renderer/command/command_processing_time_estimator.cpp @@ -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((static_cast(sample_count) * 60.0f) * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const BiquadFilterAndMixCommand& command) const { + return static_cast((static_cast(sample_count) * 60.0f) * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const BiquadFilterAndMixCommand& command) const { + return static_cast((static_cast(sample_count) * 60.0f) * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const BiquadFilterAndMixCommand& command) const { + return static_cast((static_cast(sample_count) * 60.0f) * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate( + [[maybe_unused]] const BiquadFilterAndMixCommand& command) const { + return static_cast((static_cast(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((static_cast(sample_count) * 100.0f) * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion2::Estimate( + [[maybe_unused]] const MultiTapBiquadFilterAndMixCommand& command) const { + return static_cast((static_cast(sample_count) * 100.0f) * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion3::Estimate( + [[maybe_unused]] const MultiTapBiquadFilterAndMixCommand& command) const { + return static_cast((static_cast(sample_count) * 100.0f) * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion4::Estimate( + [[maybe_unused]] const MultiTapBiquadFilterAndMixCommand& command) const { + return static_cast((static_cast(sample_count) * 100.0f) * 1.2f); +} + +u32 CommandProcessingTimeEstimatorVersion5::Estimate( + [[maybe_unused]] const MultiTapBiquadFilterAndMixCommand& command) const { + return static_cast((static_cast(sample_count) * 100.0f) * 1.2f); +} + } // namespace AudioCore::Renderer diff --git a/src/audio_core/renderer/command/command_processing_time_estimator.h b/src/audio_core/renderer/command/command_processing_time_estimator.h index 1c76e4ba4..275e9d684 100644 --- a/src/audio_core/renderer/command/command_processing_time_estimator.h +++ b/src/audio_core/renderer/command/command_processing_time_estimator.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 #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{}; diff --git a/src/audio_core/renderer/command/commands.h b/src/audio_core/renderer/command/commands.h index 6d8b8546d..17a093b60 100644 --- a/src/audio_core/renderer/command/commands.h +++ b/src/audio_core/renderer/command/commands.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 #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" diff --git a/src/audio_core/renderer/command/effect/biquad_filter.cpp b/src/audio_core/renderer/command/effect/biquad_filter.cpp index d40fa71b7..3850f21ea 100644 --- a/src/audio_core/renderer/command/effect/biquad_filter.cpp +++ b/src/audio_core/renderer/command/effect/biquad_filter.cpp @@ -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 output, std::span input, state.s3 = Common::BitCast(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 output, std::span input, + std::array& b_, std::array& a_, + VoiceState::BiquadFilterState& state, const u32 sample_count, + f32 volume) { + constexpr f64 min{std::numeric_limits::min()}; + constexpr f64 max{std::numeric_limits::max()}; + std::array 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 a{Common::FixedPoint<50, 14>::from_base(a_[0]).to_double(), + Common::FixedPoint<50, 14>::from_base(a_[1]).to_double()}; + std::array s{Common::BitCast(state.s0), Common::BitCast(state.s1), + Common::BitCast(state.s2), Common::BitCast(state.s3)}; + + for (u32 i = 0; i < sample_count; i++) { + f64 in_sample{static_cast(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(output[i]) + filtered * static_cast(volume)}; + output[i] = static_cast(std::clamp(mixed, min, max)); + } + + state.s0 = Common::BitCast(s[0]); + state.s1 = Common::BitCast(s[1]); + state.s2 = Common::BitCast(s[2]); + state.s3 = Common::BitCast(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 output, std::span input, + std::array& b_, std::array& a_, + VoiceState::BiquadFilterState& state, const u32 sample_count, + f32 volume, f32 ramp) { + constexpr f64 min{std::numeric_limits::min()}; + constexpr f64 max{std::numeric_limits::max()}; + std::array 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 a{Common::FixedPoint<50, 14>::from_base(a_[0]).to_double(), + Common::FixedPoint<50, 14>::from_base(a_[1]).to_double()}; + std::array s{Common::BitCast(state.s0), Common::BitCast(state.s1), + Common::BitCast(state.s2), Common::BitCast(state.s3)}; + + f32 current_volume{volume}; + f32 last_mixed{0.0f}; + + for (u32 i = 0; i < sample_count; i++) { + f64 in_sample{static_cast(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(filtered * static_cast(current_volume)); + f64 mixed{static_cast(output[i]) + static_cast(last_mixed)}; + output[i] = static_cast(std::clamp(mixed, min, max)); + + current_volume += ramp; + } + + state.s0 = Common::BitCast(s[0]); + state.s1 = Common::BitCast(s[1]); + state.s2 = Common::BitCast(s[2]); + state.s3 = Common::BitCast(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 output, std::span input, + std::array& biquads, + std::array& states, + const u32 sample_count, f32 volume) { + constexpr f64 min{std::numeric_limits::min()}; + constexpr f64 max{std::numeric_limits::max()}; + + // Convert first filter coefficients + std::array 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 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 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 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 s0{Common::BitCast(states[0].s0), Common::BitCast(states[0].s1), + Common::BitCast(states[0].s2), Common::BitCast(states[0].s3)}; + std::array s1{Common::BitCast(states[1].s0), Common::BitCast(states[1].s1), + Common::BitCast(states[1].s2), Common::BitCast(states[1].s3)}; + + for (u32 i = 0; i < sample_count; i++) { + f64 in_sample{static_cast(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(output[i]) + filtered1 * static_cast(volume)}; + output[i] = static_cast(std::clamp(mixed, min, max)); + } + + // Save states back + states[0].s0 = Common::BitCast(s0[0]); + states[0].s1 = Common::BitCast(s0[1]); + states[0].s2 = Common::BitCast(s0[2]); + states[0].s3 = Common::BitCast(s0[3]); + states[1].s0 = Common::BitCast(s1[0]); + states[1].s1 = Common::BitCast(s1[1]); + states[1].s2 = Common::BitCast(s1[2]); + states[1].s3 = Common::BitCast(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 output, std::span input, + std::array& biquads, + std::array& states, + const u32 sample_count, f32 volume, f32 ramp) { + constexpr f64 min{std::numeric_limits::min()}; + constexpr f64 max{std::numeric_limits::max()}; + + // Convert first filter coefficients + std::array 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 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 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 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 s0{Common::BitCast(states[0].s0), Common::BitCast(states[0].s1), + Common::BitCast(states[0].s2), Common::BitCast(states[0].s3)}; + std::array s1{Common::BitCast(states[1].s0), Common::BitCast(states[1].s1), + Common::BitCast(states[1].s2), Common::BitCast(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(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(filtered1 * static_cast(current_volume)); + f64 mixed{static_cast(output[i]) + static_cast(last_mixed)}; + output[i] = static_cast(std::clamp(mixed, min, max)); + + current_volume += ramp; + } + + // Save states back + states[0].s0 = Common::BitCast(s0[0]); + states[0].s1 = Common::BitCast(s0[1]); + states[0].s2 = Common::BitCast(s0[2]); + states[0].s3 = Common::BitCast(s0[3]); + states[1].s0 = Common::BitCast(s1[0]); + states[1].s1 = Common::BitCast(s1[1]); + states[1].s2 = Common::BitCast(s1[2]); + states[1].s3 = Common::BitCast(s1[3]); + + return last_mixed; +} + /** * Biquad filter s32 implementation. * diff --git a/src/audio_core/renderer/command/effect/biquad_filter.h b/src/audio_core/renderer/command/effect/biquad_filter.h index ceef8a1bd..204aacc7b 100644 --- a/src/audio_core/renderer/command/effect/biquad_filter.h +++ b/src/audio_core/renderer/command/effect/biquad_filter.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 #pragma once @@ -90,4 +91,70 @@ void ApplyBiquadFilterFloat2(std::span output, std::span input, std::array& b, std::array& 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 output, std::span input, + std::array& b, std::array& 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 output, std::span input, + std::array& b, std::array& 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 output, std::span input, + std::array& biquads, + std::array& 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 output, std::span input, + std::array& biquads, + std::array& states, + const u32 sample_count, f32 volume, f32 ramp); + } // namespace AudioCore::Renderer diff --git a/src/audio_core/renderer/command/effect/biquad_filter_and_mix.cpp b/src/audio_core/renderer/command/effect/biquad_filter_and_mix.cpp new file mode 100644 index 000000000..7ddb1b9af --- /dev/null +++ b/src/audio_core/renderer/command/effect/biquad_filter_and_mix.cpp @@ -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(state)}; + auto* prev_state_{reinterpret_cast(previous_state)}; + auto* voice_state_{reinterpret_cast(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(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(voice_state_->previous_samples.size())) { + voice_state_->previous_samples[last_sample_index] = static_cast(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 diff --git a/src/audio_core/renderer/command/effect/biquad_filter_and_mix.h b/src/audio_core/renderer/command/effect/biquad_filter_and_mix.h new file mode 100644 index 000000000..79316dba4 --- /dev/null +++ b/src/audio_core/renderer/command/effect/biquad_filter_and_mix.h @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +#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 diff --git a/src/audio_core/renderer/command/effect/multi_tap_biquad_filter_and_mix.cpp b/src/audio_core/renderer/command/effect/multi_tap_biquad_filter_and_mix.cpp new file mode 100644 index 000000000..2a5de67e3 --- /dev/null +++ b/src/audio_core/renderer/command/effect/multi_tap_biquad_filter_and_mix.cpp @@ -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 states_{}; + std::array prev_states_{}; + auto* voice_state_{reinterpret_cast(voice_state)}; + + for (u32 i = 0; i < MaxBiquadFilters; i++) { + states_[i] = reinterpret_cast(states[i]); + prev_states_[i] = reinterpret_cast(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 states_array{}; + for (u32 i = 0; i < MaxBiquadFilters; i++) { + states_array[i] = *states_[i]; + } + + if (has_volume_ramp) { + f32 ramp = (volume1 - volume0) / static_cast(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(voice_state_->previous_samples.size())) { + voice_state_->previous_samples[last_sample_index] = static_cast(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 diff --git a/src/audio_core/renderer/command/effect/multi_tap_biquad_filter_and_mix.h b/src/audio_core/renderer/command/effect/multi_tap_biquad_filter_and_mix.h new file mode 100644 index 000000000..ed3241aac --- /dev/null +++ b/src/audio_core/renderer/command/effect/multi_tap_biquad_filter_and_mix.h @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +#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 biquads; + /// Biquad states, updated each call + std::array states; + /// Previous biquad states (for state restoration) + std::array 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 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 diff --git a/src/audio_core/renderer/command/icommand.h b/src/audio_core/renderer/command/icommand.h index 10a78ddf2..9d3251158 100644 --- a/src/audio_core/renderer/command/icommand.h +++ b/src/audio_core/renderer/command/icommand.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 #pragma once @@ -47,6 +48,8 @@ enum class CommandId : u8 { /* 0x1C */ MultiTapBiquadFilter, /* 0x1D */ Capture, /* 0x1E */ Compressor, + /* 0x1F */ BiquadFilterAndMix, + /* 0x20 */ MultiTapBiquadFilterAndMix, }; constexpr u32 CommandMagic{0xCAFEBABE}; diff --git a/src/audio_core/renderer/splitter/splitter_context.cpp b/src/audio_core/renderer/splitter/splitter_context.cpp index a984ce121..0fe513d66 100644 --- a/src/audio_core/renderer/splitter/splitter_context.cpp +++ b/src/audio_core/renderer/splitter/splitter_context.cpp @@ -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 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 splitter_bqf_states_) { if (behavior.IsSplitterSupported() && params.splitter_infos > 0 && params.splitter_destinations > 0) { splitter_infos = allocator.Allocate(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(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(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(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(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(1 << 14); + out.numerator[0] = static_cast(legacy.b[0]) * scale; + out.numerator[1] = static_cast(legacy.b[1]) * scale; + out.numerator[2] = static_cast(legacy.b[2]) * scale; + out.denominator[0] = static_cast(legacy.a[0]) * scale; + out.denominator[1] = static_cast(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(destinations_count / info_count); } +std::span SplitterContext::GetBiquadFilterState(s32 destination_id) { + if (splitter_bqf_states.empty() || destination_id < 0 || + destination_id >= static_cast(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; } diff --git a/src/audio_core/renderer/splitter/splitter_context.h b/src/audio_core/renderer/splitter/splitter_context.h index 6f5ec6574..414edd375 100644 --- a/src/audio_core/renderer/splitter/splitter_context.h +++ b/src/audio_core/renderer/splitter/splitter_context.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 #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 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 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 splitter_bqf_states{}; }; } // namespace Renderer diff --git a/src/audio_core/renderer/splitter/splitter_destinations_data.cpp b/src/audio_core/renderer/splitter/splitter_destinations_data.cpp index ed6dd3bf0..56048463e 100644 --- a/src/audio_core/renderer/splitter/splitter_destinations_data.cpp +++ b/src/audio_core/renderer/splitter/splitter_destinations_data.cpp @@ -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 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 SplitterDestina return biquad_filters; } +std::span SplitterDestinationData::GetBiquadFiltersRev12() { + return biquad_filters_rev12; +} + +std::span SplitterDestinationData::GetBiquadFiltersRev12() const { + return biquad_filters_rev12; +} + } // namespace AudioCore::Renderer diff --git a/src/audio_core/renderer/splitter/splitter_destinations_data.h b/src/audio_core/renderer/splitter/splitter_destinations_data.h index 1094d08ea..8c91d7b03 100644 --- a/src/audio_core/renderer/splitter/splitter_destinations_data.h +++ b/src/audio_core/renderer/splitter/splitter_destinations_data.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 #pragma once @@ -7,13 +8,11 @@ #include #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 mix_volumes; + /* 0x68 */ u32 mix_id; + /* 0x6C */ std::array 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 GetBiquadFilters() const; + /** + * Get biquad filter parameters for this destination (REV12). + * + * @return Span of biquad filter parameters. + */ + std::span GetBiquadFiltersRev12(); + + /** + * Get const biquad filter parameters for this destination (REV12). + * + * @return Const span of biquad filter parameters. + */ + std::span GetBiquadFiltersRev12() const; + private: /// Id of this destination const s32 id; @@ -171,7 +206,9 @@ private: std::array mix_volumes{0.0f}; /// Previous mix volumes std::array prev_mix_volumes{0.0f}; - /// Biquad filter parameters (REV15+) + /// Biquad filter parameters (REV12, fixed-point) + std::array biquad_filters_rev12{}; + /// Biquad filter parameters (REV15+, float) std::array biquad_filters{}; /// Next destination in the mix chain SplitterDestinationData* next{}; diff --git a/src/audio_core/renderer/system.cpp b/src/audio_core/renderer/system.cpp index c30d68426..4e8dc13a9 100644 --- a/src/audio_core/renderer/system.cpp +++ b/src/audio_core/renderer/system.cpp @@ -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 @@ -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 splitter_bqf_states{}; + if (behavior.IsBiquadFilterParameterForSplitterEnabled() && + params.splitter_destinations > 0) { + splitter_bqf_states = allocator.Allocate( + 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 effect_result_states_cpu{}; if (behavior.IsEffectInfoVersion2Supported() && params.effects > 0) { @@ -451,6 +474,9 @@ Result System::Update(std::span input, std::span 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)};