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:
Zephyron
2026-01-03 05:35:02 +00:00
23 changed files with 1163 additions and 27 deletions

View File

@@ -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

View File

@@ -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},
}};

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#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

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@@ -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

View File

@@ -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;

View File

@@ -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) {

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@@ -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.
*

View File

@@ -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);
}
}
}
}

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#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

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@@ -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{};

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@@ -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"

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#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.
*

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@@ -47,6 +48,8 @@ enum class CommandId : u8 {
/* 0x1C */ MultiTapBiquadFilter,
/* 0x1D */ Capture,
/* 0x1E */ Compressor,
/* 0x1F */ BiquadFilterAndMix,
/* 0x20 */ MultiTapBiquadFilterAndMix,
};
constexpr u32 CommandMagic{0xCAFEBABE};

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#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;
}

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@@ -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

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#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

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
@@ -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{};

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2026 citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#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)};