feat: REV15 audio renderer + HID fix (v0.8.0)

Complete SDK15 audio implementation with native float biquads.
Fixes TotK 1.4.2 and BotW 1.8.2 boot loops.

- Add REV15 float biquad filter support
- Implement VoiceInParameterV2 (0x188 bytes)
- Add SplitterDestinationV2b with biquad filters
- Fix HID sampling number (double state value)
- Add AudioSnoopManager and AudioSystemManager
- Implement FinalOutputRecorder system
- Add FFT and loudness calculator (ITU-R BS.1770)
- Add full Limiter effect

Resolves boot loops and controller detection in SDK20 games.

Signed-off-by: Zephyron <zephyron@citron-emu.org>
This commit is contained in:
Zephyron
2025-10-11 14:10:08 +10:00
parent 3c16d8330b
commit 56dba09e0c
37 changed files with 2769 additions and 34 deletions

View File

@@ -198,4 +198,12 @@ bool BehaviorInfo::IsSplitterPrevVolumeResetSupported() const {
return CheckFeatureSupported(SupportTags::SplitterPrevVolumeReset, user_revision);
}
bool BehaviorInfo::IsSplitterDestinationV2bSupported() const {
return CheckFeatureSupported(SupportTags::SplitterDestinationV2b, user_revision);
}
bool BehaviorInfo::IsVoiceInParameterV2Supported() const {
return CheckFeatureSupported(SupportTags::VoiceInParameterV2, user_revision);
}
} // namespace AudioCore::Renderer

View File

@@ -378,6 +378,22 @@ public:
*/
bool IsSplitterPrevVolumeResetSupported() const;
/**
* Check if splitter destination v2b parameter format is supported (revision 15+).
* This uses the extended parameter format with biquad filter fields.
*
* @return True if supported, otherwise false.
*/
bool IsSplitterDestinationV2bSupported() const;
/**
* Check if voice input parameter v2 format is supported (revision 15+).
* This uses the extended parameter format with float biquad filters.
*
* @return True if supported, otherwise false.
*/
bool IsVoiceInParameterV2Supported() const;
/// Host version
u32 process_revision;
/// User version

View File

@@ -61,8 +61,6 @@ Result InfoUpdater::UpdateVoices(VoiceContext& voice_context,
const PoolMapper pool_mapper(process_handle, memory_pools, memory_pool_count,
behaviour.IsMemoryForceMappingEnabled());
const auto voice_count{voice_context.GetCount()};
std::span<const VoiceInfo::InParameter> in_params{
reinterpret_cast<const VoiceInfo::InParameter*>(input), voice_count};
std::span<VoiceInfo::OutStatus> out_params{reinterpret_cast<VoiceInfo::OutStatus*>(output),
voice_count};
@@ -73,8 +71,97 @@ Result InfoUpdater::UpdateVoices(VoiceContext& voice_context,
u32 new_voice_count{0};
// Two input formats exist: legacy (0x170) and v2 with float biquad (0x188).
const bool use_v2 = behaviour.IsVoiceInParameterV2Supported();
const u32 in_stride = use_v2 ? 0x188u : static_cast<u32>(sizeof(VoiceInfo::InParameter));
for (u32 i = 0; i < voice_count; i++) {
const auto& in_param{in_params[i]};
VoiceInfo::InParameter local_in{};
// Store original float biquad coefficients for REV15+
std::array<VoiceInfo::BiquadFilterParameter2, MaxBiquadFilters> float_biquads{};
if (!use_v2) {
const auto* in_param_ptr = reinterpret_cast<const VoiceInfo::InParameter*>(input + i * sizeof(VoiceInfo::InParameter));
local_in = *in_param_ptr;
} else {
struct VoiceInParameterV2 {
u32 id;
u32 node_id;
bool is_new;
bool in_use;
PlayState play_state;
SampleFormat sample_format;
u32 sample_rate;
u32 priority;
u32 sort_order;
u32 channel_count;
f32 pitch;
f32 volume;
// Two BiquadFilterParameter2 (0x18 each) -> ignored/converted
struct BiquadV2 { bool enable; u8 r1; u8 r2; u8 r3; std::array<f32,3> b; std::array<f32,2> a; } biquads[2];
u32 wave_buffer_count;
u32 wave_buffer_index;
u32 reserved1;
u64 src_data_address;
u64 src_data_size;
s32 mix_id;
u32 splitter_id;
std::array<VoiceInfo::WaveBufferInternal, MaxWaveBuffers> wavebuffers;
std::array<u32, MaxChannels> channel_resource_ids;
bool clear_voice_drop;
u8 flush_wave_buffer_count;
u16 reserved2;
VoiceInfo::Flags flags;
SrcQuality src_quality;
u32 external_ctx;
u32 external_ctx_size;
u32 reserved3[2];
};
const auto* vin = reinterpret_cast<const VoiceInParameterV2*>(input + i * in_stride);
local_in.id = vin->id;
local_in.node_id = vin->node_id;
local_in.is_new = vin->is_new;
local_in.in_use = vin->in_use;
local_in.play_state = vin->play_state;
local_in.sample_format = vin->sample_format;
local_in.sample_rate = vin->sample_rate;
local_in.priority = static_cast<s32>(vin->priority);
local_in.sort_order = static_cast<s32>(vin->sort_order);
local_in.channel_count = vin->channel_count;
local_in.pitch = vin->pitch;
local_in.volume = vin->volume;
// For REV15+, we keep float coefficients separate and only convert for compatibility
for (size_t filter_idx = 0; filter_idx < MaxBiquadFilters; filter_idx++) {
const auto& src = vin->biquads[filter_idx];
auto& dst = local_in.biquads[filter_idx];
dst.enabled = src.enable;
// Convert float coefficients to fixed-point Q2.14 for legacy path
dst.b[0] = static_cast<s16>(std::clamp(src.b[0] * 16384.0f, -32768.0f, 32767.0f));
dst.b[1] = static_cast<s16>(std::clamp(src.b[1] * 16384.0f, -32768.0f, 32767.0f));
dst.b[2] = static_cast<s16>(std::clamp(src.b[2] * 16384.0f, -32768.0f, 32767.0f));
dst.a[0] = static_cast<s16>(std::clamp(src.a[0] * 16384.0f, -32768.0f, 32767.0f));
dst.a[1] = static_cast<s16>(std::clamp(src.a[1] * 16384.0f, -32768.0f, 32767.0f));
// Also store the native float version
float_biquads[filter_idx].enabled = src.enable;
float_biquads[filter_idx].numerator = src.b;
float_biquads[filter_idx].denominator = src.a;
}
local_in.wave_buffer_count = vin->wave_buffer_count;
local_in.wave_buffer_index = static_cast<u16>(vin->wave_buffer_index);
local_in.src_data_address = static_cast<CpuAddr>(vin->src_data_address);
local_in.src_data_size = vin->src_data_size;
local_in.mix_id = static_cast<u32>(vin->mix_id);
local_in.splitter_id = vin->splitter_id;
local_in.wave_buffer_internal = vin->wavebuffers;
local_in.channel_resource_ids = vin->channel_resource_ids;
local_in.clear_voice_drop = vin->clear_voice_drop;
local_in.flush_buffer_count = vin->flush_wave_buffer_count;
local_in.flags = vin->flags;
local_in.src_quality = vin->src_quality;
}
const auto& in_param = local_in;
std::array<VoiceState*, MaxChannels> voice_states{};
if (!in_param.in_use) {
@@ -98,6 +185,14 @@ Result InfoUpdater::UpdateVoices(VoiceContext& voice_context,
BehaviorInfo::ErrorInfo update_error{};
voice_info.UpdateParameters(update_error, in_param, pool_mapper, behaviour);
// For REV15+, store the native float biquad coefficients
if (use_v2) {
voice_info.use_float_biquads = true;
voice_info.biquads_float = float_biquads;
} else {
voice_info.use_float_biquads = false;
}
if (!update_error.error_code.IsSuccess()) {
behaviour.AppendError(update_error);
}
@@ -118,7 +213,7 @@ Result InfoUpdater::UpdateVoices(VoiceContext& voice_context,
new_voice_count += in_param.channel_count;
}
auto consumed_input_size{voice_count * static_cast<u32>(sizeof(VoiceInfo::InParameter))};
auto consumed_input_size{voice_count * in_stride};
auto consumed_output_size{voice_count * static_cast<u32>(sizeof(VoiceInfo::OutStatus))};
if (consumed_input_size != in_header->voices_size) {
LOG_ERROR(Service_Audio, "Consumed an incorrect voices size, header size={}, consumed={}",
@@ -254,18 +349,29 @@ Result InfoUpdater::UpdateMixes(MixContext& mix_context, const u32 mix_buffer_co
EffectContext& effect_context, SplitterContext& splitter_context) {
s32 mix_count{0};
u32 consumed_input_size{0};
u32 input_mix_size{0};
if (behaviour.IsMixInParameterDirtyOnlyUpdateSupported()) {
auto in_dirty_params{reinterpret_cast<const MixInfo::InDirtyParameter*>(input)};
mix_count = in_dirty_params->count;
// Validate against expected header size to ensure structure is correct
if (mix_count < 0 || mix_count > 0x100) {
LOG_ERROR(Service_Audio,
"Invalid mix count from dirty parameter: count={}, magic=0x{:X}, expected_size={}",
mix_count, in_dirty_params->magic, in_header->mix_size);
return Service::Audio::ResultInvalidUpdateInfo;
}
consumed_input_size += static_cast<u32>(sizeof(MixInfo::InDirtyParameter));
input += sizeof(MixInfo::InDirtyParameter);
consumed_input_size = static_cast<u32>(sizeof(MixInfo::InDirtyParameter) +
mix_count * sizeof(MixInfo::InParameter));
} else {
mix_count = mix_context.GetCount();
consumed_input_size = static_cast<u32>(mix_count * sizeof(MixInfo::InParameter));
}
input_mix_size = static_cast<u32>(mix_count * sizeof(MixInfo::InParameter));
consumed_input_size += input_mix_size;
if (mix_buffer_count == 0) {
return Service::Audio::ResultInvalidUpdateInfo;
}
@@ -330,7 +436,7 @@ Result InfoUpdater::UpdateMixes(MixContext& mix_context, const u32 mix_buffer_co
return Service::Audio::ResultInvalidUpdateInfo;
}
input += mix_count * sizeof(MixInfo::InParameter);
input += input_mix_size;
return ResultSuccess;
}

View File

@@ -234,6 +234,14 @@ void CommandBuffer::GenerateBiquadFilterCommand(const s32 node_id, VoiceInfo& vo
cmd.biquad = voice_info.biquads[biquad_index];
// REV15+: Use native float coefficients if available
if (voice_info.use_float_biquads) {
cmd.biquad_float = voice_info.biquads_float[biquad_index];
cmd.use_float_coefficients = true;
} else {
cmd.use_float_coefficients = false;
}
cmd.state = memory_pool->Translate(CpuAddr(voice_state.biquad_states[biquad_index].data()),
MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState));
@@ -260,6 +268,9 @@ void CommandBuffer::GenerateBiquadFilterCommand(const s32 node_id, EffectInfoBas
cmd.biquad.b = parameter.b;
cmd.biquad.a = parameter.a;
// Effects use legacy fixed-point format
cmd.use_float_coefficients = false;
cmd.state = memory_pool->Translate(CpuAddr(state),
MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState));
@@ -655,6 +666,14 @@ void CommandBuffer::GenerateMultitapBiquadFilterCommand(const s32 node_id, Voice
cmd.output = buffer_count + channel;
cmd.biquads = voice_info.biquads;
// REV15+: Use native float coefficients if available
if (voice_info.use_float_biquads) {
cmd.biquads_float = voice_info.biquads_float;
cmd.use_float_coefficients = true;
} else {
cmd.use_float_coefficients = false;
}
cmd.states[0] =
memory_pool->Translate(CpuAddr(voice_state.biquad_states[0].data()),
MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState));

View File

@@ -48,6 +48,39 @@ void ApplyBiquadFilterFloat(std::span<s32> output, std::span<const s32> input,
state.s3 = Common::BitCast<s64>(s[3]);
}
/**
* Biquad filter float implementation with native float coefficients (SDK REV15+).
*/
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) {
constexpr f64 min{std::numeric_limits<s32>::min()};
constexpr f64 max{std::numeric_limits<s32>::max()};
std::array<f64, 3> b_double{static_cast<f64>(b[0]), static_cast<f64>(b[1]), static_cast<f64>(b[2])};
std::array<f64, 2> a_double{static_cast<f64>(a[0]), static_cast<f64>(a[1])};
std::array<f64, 4> s{Common::BitCast<f64>(state.s0), Common::BitCast<f64>(state.s1),
Common::BitCast<f64>(state.s2), Common::BitCast<f64>(state.s3)};
for (u32 i = 0; i < sample_count; i++) {
f64 in_sample{static_cast<f64>(input[i])};
auto sample{in_sample * b_double[0] + s[0] * b_double[1] + s[1] * b_double[2] +
s[2] * a_double[0] + s[3] * a_double[1]};
output[i] = static_cast<s32>(std::clamp(sample, min, max));
s[1] = s[0];
s[0] = in_sample;
s[3] = s[2];
s[2] = sample;
}
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]);
}
/**
* Biquad filter s32 implementation.
*
@@ -95,8 +128,14 @@ void BiquadFilterCommand::Process(const AudioRenderer::CommandListProcessor& pro
processor.mix_buffers.subspan(output * processor.sample_count, processor.sample_count)};
if (use_float_processing) {
ApplyBiquadFilterFloat(output_buffer, input_buffer, biquad.b, biquad.a, *state_,
processor.sample_count);
// REV15+: Use native float coefficients if available
if (use_float_coefficients) {
ApplyBiquadFilterFloat2(output_buffer, input_buffer, biquad_float.numerator,
biquad_float.denominator, *state_, processor.sample_count);
} else {
ApplyBiquadFilterFloat(output_buffer, input_buffer, biquad.b, biquad.a, *state_,
processor.sample_count);
}
} else {
ApplyBiquadFilterInt(output_buffer, input_buffer, biquad.b, biquad.a, *state_,
processor.sample_count);

View File

@@ -48,14 +48,18 @@ struct BiquadFilterCommand : ICommand {
s16 input;
/// Output mix buffer index
s16 output;
/// Input parameters for biquad
/// Input parameters for biquad (legacy fixed-point)
VoiceInfo::BiquadFilterParameter biquad;
/// Input parameters for biquad (REV15+ native float)
VoiceInfo::BiquadFilterParameter2 biquad_float;
/// Biquad state, updated each call
CpuAddr state;
/// If true, reset the state
bool needs_init;
/// If true, use float processing rather than int
bool use_float_processing;
/// If true, use native float coefficients (REV15+)
bool use_float_coefficients;
};
/**
@@ -72,4 +76,18 @@ void ApplyBiquadFilterFloat(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);
/**
* Biquad filter float implementation with native float coefficients (SDK REV15+).
*
* @param output - Output container for filtered samples.
* @param input - Input container for samples to be filtered.
* @param b - Feedforward coefficients (float).
* @param a - Feedback coefficients (float).
* @param state - State to track previous samples.
* @param sample_count - Number of samples to process.
*/
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);
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,90 @@
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <cmath>
#include <span>
#include "audio_core/adsp/apps/audio_renderer/command_list_processor.h"
#include "audio_core/renderer/command/effect/limiter.h"
#include "core/memory.h"
namespace AudioCore::Renderer {
void LimiterCommand::Dump([[maybe_unused]] const AudioRenderer::CommandListProcessor& processor,
std::string& string) {
string += fmt::format("LimiterCommand\n\tenabled {} channels {}\n", effect_enabled,
parameter.channel_count);
}
void LimiterCommand::Process(const AudioRenderer::CommandListProcessor& processor) {
std::array<std::span<const s32>, MaxChannels> input_buffers{};
std::array<std::span<s32>, MaxChannels> output_buffers{};
for (u32 i = 0; i < parameter.channel_count; i++) {
input_buffers[i] = processor.mix_buffers.subspan(inputs[i] * processor.sample_count,
processor.sample_count);
output_buffers[i] = processor.mix_buffers.subspan(outputs[i] * processor.sample_count,
processor.sample_count);
}
auto state_buffer{reinterpret_cast<LimiterInfo::State*>(state)};
if (effect_enabled) {
// Convert parameters
const f32 attack_coeff =
std::exp(-1.0f / (parameter.attack_time * processor.target_sample_rate / 1000.0f));
const f32 release_coeff =
std::exp(-1.0f / (parameter.release_time * processor.target_sample_rate / 1000.0f));
const f32 threshold_linear = std::pow(10.0f, parameter.threshold / 20.0f);
const f32 makeup_gain_linear = std::pow(10.0f, parameter.makeup_gain / 20.0f);
for (u32 sample = 0; sample < processor.sample_count; sample++) {
// Find peak across all channels
f32 peak = 0.0f;
for (u32 ch = 0; ch < parameter.channel_count; ch++) {
const f32 abs_sample = std::abs(static_cast<f32>(input_buffers[ch][sample]));
peak = std::max(peak, abs_sample);
}
// Update envelope
if (peak > state_buffer->envelope) {
state_buffer->envelope =
attack_coeff * state_buffer->envelope + (1.0f - attack_coeff) * peak;
} else {
state_buffer->envelope =
release_coeff * state_buffer->envelope + (1.0f - release_coeff) * peak;
}
// Calculate gain reduction
f32 gain = 1.0f;
if (state_buffer->envelope > threshold_linear) {
const f32 over = state_buffer->envelope / threshold_linear;
gain = 1.0f / std::pow(over, (parameter.ratio - 1.0f) / parameter.ratio);
}
state_buffer->gain_reduction = gain;
// Apply limiting with makeup gain
const f32 total_gain = gain * makeup_gain_linear;
for (u32 ch = 0; ch < parameter.channel_count; ch++) {
output_buffers[ch][sample] =
static_cast<s32>(input_buffers[ch][sample] * total_gain);
}
}
} else {
// Bypass: just copy input to output
for (u32 ch = 0; ch < parameter.channel_count; ch++) {
if (inputs[ch] != outputs[ch]) {
std::memcpy(output_buffers[ch].data(), input_buffers[ch].data(),
output_buffers[ch].size_bytes());
}
}
}
}
bool LimiterCommand::Verify(const AudioRenderer::CommandListProcessor& processor) {
return true;
}
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <string>
#include "audio_core/renderer/command/icommand.h"
#include "audio_core/renderer/effect/limiter.h"
#include "common/common_types.h"
namespace AudioCore::ADSP::AudioRenderer {
class CommandListProcessor;
}
namespace AudioCore::Renderer {
/**
* AudioRenderer command for limiting volume with attack/release controls.
*/
struct LimiterCommand : 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 offsets for each channel
std::array<s16, MaxChannels> inputs;
/// Output mix buffer offsets for each channel
std::array<s16, MaxChannels> outputs;
/// Input parameters
LimiterInfo::ParameterVersion2 parameter;
/// State, updated each call
CpuAddr state;
/// Game-supplied workbuffer (Unused)
CpuAddr workbuffer;
/// Is this effect enabled?
bool effect_enabled;
};
} // namespace AudioCore::Renderer

View File

@@ -33,8 +33,14 @@ void MultiTapBiquadFilterCommand::Process(const AudioRenderer::CommandListProces
*state = {};
}
ApplyBiquadFilterFloat(output_buffer, input_buffer, biquads[i].b, biquads[i].a, *state,
processor.sample_count);
// REV15+: Use native float coefficients if available
if (use_float_coefficients) {
ApplyBiquadFilterFloat2(output_buffer, input_buffer, biquads_float[i].numerator,
biquads_float[i].denominator, *state, processor.sample_count);
} else {
ApplyBiquadFilterFloat(output_buffer, input_buffer, biquads[i].b, biquads[i].a, *state,
processor.sample_count);
}
}
}

View File

@@ -47,14 +47,18 @@ struct MultiTapBiquadFilterCommand : ICommand {
s16 input;
/// Output mix buffer index
s16 output;
/// Biquad parameters
/// Biquad parameters (legacy fixed-point)
std::array<VoiceInfo::BiquadFilterParameter, MaxBiquadFilters> biquads;
/// Biquad parameters (REV15+ native float)
std::array<VoiceInfo::BiquadFilterParameter2, MaxBiquadFilters> biquads_float;
/// Biquad states, updated each call
std::array<CpuAddr, MaxBiquadFilters> states;
/// If each biquad needs initialisation
std::array<bool, MaxBiquadFilters> needs_init;
/// Number of active biquads
u8 filter_tap_count;
/// If true, use native float coefficients (REV15+)
bool use_float_coefficients;
};
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,67 @@
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/renderer/effect/limiter.h"
#include "core/hle/result.h"
namespace AudioCore::Renderer {
void LimiterInfo::Update(BehaviorInfo::ErrorInfo& error_info,
const EffectInfoBase::InParameterVersion1& in_params,
const PoolMapper& pool_mapper) {
auto in_specific{reinterpret_cast<const ParameterVersion1*>(in_params.specific.data())};
auto params{reinterpret_cast<ParameterVersion1*>(parameter.data())};
std::memcpy(params, in_specific, sizeof(ParameterVersion1));
mix_id = in_params.mix_id;
process_order = in_params.process_order;
enabled = in_params.enabled;
error_info.error_code = ResultSuccess;
error_info.address = CpuAddr(0);
}
void LimiterInfo::Update(BehaviorInfo::ErrorInfo& error_info,
const EffectInfoBase::InParameterVersion2& in_params,
const PoolMapper& pool_mapper) {
auto in_specific{reinterpret_cast<const ParameterVersion2*>(in_params.specific.data())};
auto params{reinterpret_cast<ParameterVersion2*>(parameter.data())};
std::memcpy(params, in_specific, sizeof(ParameterVersion2));
mix_id = in_params.mix_id;
process_order = in_params.process_order;
enabled = in_params.enabled;
error_info.error_code = ResultSuccess;
error_info.address = CpuAddr(0);
}
void LimiterInfo::UpdateForCommandGeneration() {
if (enabled) {
usage_state = UsageState::Enabled;
} else {
usage_state = UsageState::Disabled;
}
auto params{reinterpret_cast<ParameterVersion1*>(parameter.data())};
params->state = ParameterState::Updated;
}
void LimiterInfo::InitializeResultState(EffectResultState& result_state) {
auto limiter_state{reinterpret_cast<State*>(result_state.state.data())};
limiter_state->envelope = 1.0f;
limiter_state->gain_reduction = 1.0f;
limiter_state->peak_hold = 0.0f;
limiter_state->peak_hold_count = 0;
limiter_state->channel_peaks.fill(0.0f);
}
void LimiterInfo::UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) {
cpu_state = dsp_state;
}
CpuAddr LimiterInfo::GetWorkbuffer(s32 index) {
return GetSingleBuffer(index);
}
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,101 @@
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include "audio_core/common/common.h"
#include "audio_core/renderer/effect/effect_info_base.h"
#include "common/common_types.h"
namespace AudioCore::Renderer {
/**
* A full-featured limiter effect with attack, release, and threshold controls.
* More sophisticated than LightLimiter.
*/
class LimiterInfo : public EffectInfoBase {
public:
struct ParameterVersion1 {
/* 0x00 */ std::array<s8, MaxChannels> inputs;
/* 0x06 */ std::array<s8, MaxChannels> outputs;
/* 0x0C */ u16 channel_count;
/* 0x0E */ u16 padding;
/* 0x10 */ s32 sample_rate;
/* 0x14 */ f32 attack_time; // Attack time in milliseconds
/* 0x18 */ f32 release_time; // Release time in milliseconds
/* 0x1C */ f32 threshold; // Threshold in dB
/* 0x20 */ f32 makeup_gain; // Makeup gain in dB
/* 0x24 */ f32 ratio; // Compression ratio
/* 0x28 */ ParameterState state;
/* 0x29 */ bool is_enabled;
/* 0x2A */ char unk2A[0x2];
};
static_assert(sizeof(ParameterVersion1) <= sizeof(EffectInfoBase::InParameterVersion1),
"LimiterInfo::ParameterVersion1 has the wrong size!");
using ParameterVersion2 = ParameterVersion1;
struct State {
/* 0x00 */ f32 envelope;
/* 0x04 */ f32 gain_reduction;
/* 0x08 */ f32 peak_hold;
/* 0x0C */ u32 peak_hold_count;
/* 0x10 */ std::array<f32, MaxChannels> channel_peaks;
};
static_assert(sizeof(State) <= sizeof(EffectInfoBase::State),
"LimiterInfo::State is too large!");
/**
* Update the info with new parameters.
*
* @param error_info - Output error information.
* @param in_params - Input parameters.
* @param pool_mapper - Memory pool mapper for buffers.
*/
void Update(BehaviorInfo::ErrorInfo& error_info,
const EffectInfoBase::InParameterVersion1& in_params,
const PoolMapper& pool_mapper);
/**
* Update the info with new parameters (version 2).
*
* @param error_info - Output error information.
* @param in_params - Input parameters.
* @param pool_mapper - Memory pool mapper for buffers.
*/
void Update(BehaviorInfo::ErrorInfo& error_info,
const EffectInfoBase::InParameterVersion2& in_params,
const PoolMapper& pool_mapper);
/**
* Update the usage state for command generation.
*/
void UpdateForCommandGeneration();
/**
* Initialize the result state.
*
* @param result_state - Result state to initialize.
*/
void InitializeResultState(EffectResultState& result_state);
/**
* Update the result state.
*
* @param cpu_state - CPU-side result state.
* @param dsp_state - DSP-side result state.
*/
void UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state);
/**
* Get a workbuffer address.
*
* @param index - Index of the workbuffer.
* @return Address of the workbuffer.
*/
CpuAddr GetWorkbuffer(s32 index);
};
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/common_types.h"
#include "common/swap.h"
namespace AudioCore {
struct FinalOutputRecorderBuffer {
/* 0x00 */ FinalOutputRecorderBuffer* next;
/* 0x08 */ VAddr samples;
/* 0x10 */ u64 capacity;
/* 0x18 */ u64 size;
/* 0x20 */ u64 offset;
/* 0x28 */ u64 end_timestamp;
};
static_assert(sizeof(FinalOutputRecorderBuffer) == 0x30,
"FinalOutputRecorderBuffer is an invalid size");
struct FinalOutputRecorderParameter {
/* 0x0 */ s32_le sample_rate;
/* 0x4 */ u16_le channel_count;
/* 0x6 */ u16_le reserved;
};
static_assert(sizeof(FinalOutputRecorderParameter) == 0x8,
"FinalOutputRecorderParameter is an invalid size");
struct FinalOutputRecorderParameterInternal {
/* 0x0 */ u32_le sample_rate;
/* 0x4 */ u32_le channel_count;
/* 0x8 */ u32_le sample_format;
/* 0xC */ u32_le state;
};
static_assert(sizeof(FinalOutputRecorderParameterInternal) == 0x10,
"FinalOutputRecorderParameterInternal is an invalid size");
} // namespace AudioCore

View File

@@ -0,0 +1,128 @@
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/audio_core.h"
#include "audio_core/renderer/final_output_recorder/final_output_recorder_system.h"
#include "audio_core/sink/sink.h"
#include "core/core.h"
#include "core/core_timing.h"
#include "core/hle/kernel/k_event.h"
#include "core/hle/result.h"
#include "core/memory.h"
namespace AudioCore::FinalOutputRecorder {
System::System(Core::System& system_, Kernel::KEvent* event_, size_t session_id_)
: system{system_}, buffer_event{event_}, session_id{session_id_} {}
System::~System() = default;
Result System::Initialize(const FinalOutputRecorderParameter& params, Kernel::KProcess* handle_,
u64 applet_resource_user_id_) {
handle = handle_;
applet_resource_user_id = applet_resource_user_id_;
sample_rate = TargetSampleRate;
sample_format = SampleFormat::PcmInt16;
channel_count = params.channel_count <= 2 ? 2 : 6;
buffers.clear();
state = State::Stopped;
return ResultSuccess;
}
Result System::Start() {
if (state != State::Stopped) {
return Service::Audio::ResultOperationFailed;
}
state = State::Started;
return ResultSuccess;
}
Result System::Stop() {
if (state == State::Started) {
state = State::Stopped;
buffers.clear();
if (buffer_event) {
buffer_event->Signal();
}
}
return ResultSuccess;
}
bool System::AppendBuffer(const FinalOutputRecorderBuffer& buffer, u64 tag) {
if (buffers.full()) {
return false;
}
auto buffer_copy = buffer;
buffers.push_back(buffer_copy);
if (state == State::Started) {
ring_buffer.AppendBufferForRecord(buffer_copy);
}
return true;
}
void System::ReleaseAndRegisterBuffers() {
// Release completed buffers
while (ring_buffer.HasAvailableBuffer()) {
FinalOutputRecorderBuffer buffer;
if (ring_buffer.GetReleasedBufferForRecord(buffer)) {
if (buffer_event) {
buffer_event->Signal();
}
}
}
}
bool System::FlushAudioBuffers() {
buffers.clear();
return true;
}
u32 System::GetReleasedBuffers(std::span<u64> tags) {
u32 released = 0;
while (ring_buffer.HasAvailableBuffer() && released < tags.size()) {
FinalOutputRecorderBuffer buffer;
if (ring_buffer.GetReleasedBufferForRecord(buffer)) {
tags[released] = buffer.offset; // Use offset as tag
released++;
} else {
break;
}
}
return released;
}
bool System::ContainsBuffer(VAddr buffer_address) const {
return ring_buffer.ContainsBuffer(buffer_address);
}
u64 System::GetBufferEndTime() const {
// Return the timestamp of the last recorded sample
return system.CoreTiming().GetClockTicks();
}
Result System::AttachWorkBuffer(VAddr work_buffer, u64 work_buffer_size_) {
if (work_buffer == 0 || work_buffer_size_ == 0) {
return Service::Audio::ResultInvalidHandle;
}
work_buffer_address = work_buffer;
work_buffer_size = work_buffer_size_;
// Initialize the ring buffer with the work buffer
auto& memory = system.ApplicationMemory();
ring_buffer.Initialize(memory, work_buffer, work_buffer_size, work_buffer, 0x100, 32);
return ResultSuccess;
}
} // namespace AudioCore::FinalOutputRecorder

View File

@@ -0,0 +1,197 @@
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <memory>
#include <span>
#include <string>
#include "audio_core/common/common.h"
#include "audio_core/device/audio_buffer_list.h"
#include "audio_core/device/device_session.h"
#include "audio_core/device/shared_ring_buffer.h"
#include "audio_core/renderer/final_output_recorder/final_output_recorder_buffer.h"
#include "core/hle/result.h"
namespace Core {
class System;
}
namespace Kernel {
class KEvent;
class KProcess;
} // namespace Kernel
namespace AudioCore::FinalOutputRecorder {
constexpr SessionTypes SessionType = SessionTypes::FinalOutputRecorder;
enum class State {
Started,
Stopped,
};
/**
* Controls and drives final output recording.
*/
class System {
public:
explicit System(Core::System& system, Kernel::KEvent* event, size_t session_id);
~System();
/**
* Initialize the final output recorder.
*
* @param params - Input parameters for the recorder.
* @param handle_ - Process handle for memory access.
* @param applet_resource_user_id_ - Applet resource user ID.
* @return Result code.
*/
Result Initialize(const FinalOutputRecorderParameter& params, Kernel::KProcess* handle_,
u64 applet_resource_user_id_);
/**
* Start the recorder.
*
* @return Result code.
*/
Result Start();
/**
* Stop the recorder.
*
* @return Result code.
*/
Result Stop();
/**
* Append a buffer for recording.
*
* @param buffer - Buffer to append.
* @param tag - User-defined tag for this buffer.
* @return True if the buffer was appended successfully.
*/
bool AppendBuffer(const FinalOutputRecorderBuffer& buffer, u64 tag);
/**
* Release and register buffers.
*/
void ReleaseAndRegisterBuffers();
/**
* Flush all audio buffers.
*
* @return True if buffers were flushed successfully.
*/
bool FlushAudioBuffers();
/**
* Get released buffers.
*
* @param tags - Output span to receive buffer tags.
* @return Number of buffers released.
*/
u32 GetReleasedBuffers(std::span<u64> tags);
/**
* Check if a buffer is contained in the queue.
*
* @param buffer_address - Address of the buffer to check.
* @return True if the buffer is in the queue.
*/
bool ContainsBuffer(VAddr buffer_address) const;
/**
* Get the current state.
*
* @return Current recorder state.
*/
State GetState() const {
return state;
}
/**
* Get the sample rate.
*
* @return Sample rate in Hz.
*/
u32 GetSampleRate() const {
return sample_rate;
}
/**
* Get the channel count.
*
* @return Number of channels.
*/
u32 GetChannelCount() const {
return channel_count;
}
/**
* Get the sample format.
*
* @return Sample format.
*/
SampleFormat GetSampleFormat() const {
return sample_format;
}
/**
* Get the session ID.
*
* @return Session ID.
*/
size_t GetSessionId() const {
return session_id;
}
/**
* Get the buffer end timestamp.
*
* @return End timestamp.
*/
u64 GetBufferEndTime() const;
/**
* Attach work buffer.
*
* @param work_buffer - Work buffer address.
* @param work_buffer_size - Work buffer size.
* @return Result code.
*/
Result AttachWorkBuffer(VAddr work_buffer, u64 work_buffer_size);
private:
/// Core system
Core::System& system;
/// Buffer event, signalled when a buffer is ready
Kernel::KEvent* buffer_event;
/// Session ID of this recorder
size_t session_id;
/// Device session for output
std::unique_ptr<DeviceSession> session;
/// Audio buffers
AudioBufferList<FinalOutputRecorderBuffer> buffers;
/// Shared ring buffer for recording
SharedRingBuffer ring_buffer;
/// Process handle for memory access
Kernel::KProcess* handle;
/// Applet resource user ID
u64 applet_resource_user_id;
/// Sample rate
u32 sample_rate{TargetSampleRate};
/// Channel count
u32 channel_count{2};
/// Sample format
SampleFormat sample_format{SampleFormat::PcmInt16};
/// Current state
State state{State::Stopped};
/// Work buffer address
VAddr work_buffer_address{0};
/// Work buffer size
u64 work_buffer_size{0};
};
} // namespace AudioCore::FinalOutputRecorder

View File

@@ -40,6 +40,7 @@ void SplitterContext::Setup(std::span<SplitterInfo> splitter_infos_, const u32 s
destinations_count = destination_count_;
splitter_bug_fixed = splitter_bug_fixed_;
splitter_prev_volume_reset_supported = behavior.IsSplitterPrevVolumeResetSupported();
splitter_float_coeff_supported = behavior.IsSplitterDestinationV2bSupported();
}
bool SplitterContext::UsingSplitter() const {
@@ -136,25 +137,57 @@ u32 SplitterContext::UpdateInfo(const u8* input, u32 offset, const u32 splitter_
u32 SplitterContext::UpdateData(const u8* input, u32 offset, const u32 count) {
for (u32 i = 0; i < count; i++) {
auto data_header{
reinterpret_cast<const SplitterDestinationData::InParameter*>(input + offset)};
// Version selection based on float coeff/biquad v2b support.
if (!splitter_float_coeff_supported) {
const auto* data_header =
reinterpret_cast<const SplitterDestinationData::InParameter*>(input + offset);
if (data_header->magic != GetSplitterSendDataMagic()) {
continue;
if (data_header->magic != GetSplitterSendDataMagic()) {
continue;
}
if (data_header->id < 0 || data_header->id > destinations_count) {
continue;
}
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);
offset += sizeof(SplitterDestinationData::InParameter);
} else {
// Version 2b: struct contains extra biquad filter fields
const auto* data_header_v2b =
reinterpret_cast<const SplitterDestinationData::InParameterVersion2b*>(input + offset);
if (data_header_v2b->magic != GetSplitterSendDataMagic()) {
continue;
}
if (data_header_v2b->id < 0 || data_header_v2b->id > destinations_count) {
continue;
}
// Map common fields to the old format
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;
// Store biquad filters from V2b (REV15+)
auto& destination = splitter_destinations[data_header_v2b->id];
destination.Update(mapped);
// Copy biquad filter parameters
auto biquad_filters = destination.GetBiquadFilters();
for (size_t filter_idx = 0; filter_idx < MaxBiquadFilters; filter_idx++) {
biquad_filters[filter_idx] = data_header_v2b->biquad_filters[filter_idx];
}
offset += static_cast<u32>(sizeof(SplitterDestinationData::InParameterVersion2b));
}
if (data_header->id < 0 || data_header->id > destinations_count) {
continue;
}
// Create a modified parameter that respects the behavior support
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);
offset += sizeof(SplitterDestinationData::InParameter);
}
return offset;

View File

@@ -186,6 +186,8 @@ private:
bool splitter_bug_fixed{};
/// Is explicit previous mix volume reset supported?
bool splitter_prev_volume_reset_supported{};
/// Is float coefficient/biquad filter v2b parameter supported?
bool splitter_float_coeff_supported{};
};
} // namespace Renderer

View File

@@ -87,4 +87,12 @@ void SplitterDestinationData::SetNext(SplitterDestinationData* next_) {
next = next_;
}
std::span<SplitterDestinationData::BiquadFilterParameter2> SplitterDestinationData::GetBiquadFilters() {
return biquad_filters;
}
std::span<const SplitterDestinationData::BiquadFilterParameter2> SplitterDestinationData::GetBiquadFilters() const {
return biquad_filters;
}
} // namespace AudioCore::Renderer

View File

@@ -10,12 +10,31 @@
#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.
*/
class SplitterDestinationData {
public:
/**
* Biquad filter parameter with float coefficients (SDK REV15+).
* Defined here to avoid circular dependency with VoiceInfo.
*/
struct BiquadFilterParameter2 {
/* 0x00 */ bool enabled;
/* 0x01 */ u8 reserved1;
/* 0x02 */ u8 reserved2;
/* 0x03 */ u8 reserved3;
/* 0x04 */ std::array<f32, 3> numerator; // b0, b1, b2
/* 0x10 */ std::array<f32, 2> denominator; // a1, a2 (a0 = 1)
};
static_assert(sizeof(BiquadFilterParameter2) == 0x18,
"BiquadFilterParameter2 has the wrong size!");
struct InParameter {
/* 0x00 */ u32 magic; // 'SNDD'
/* 0x04 */ s32 id;
@@ -27,6 +46,19 @@ public:
static_assert(sizeof(InParameter) == 0x70,
"SplitterDestinationData::InParameter has the wrong size!");
struct InParameterVersion2b {
/* 0x00 */ u32 magic; // 'SNDD'
/* 0x04 */ s32 id;
/* 0x08 */ std::array<f32, MaxMixBuffers> mix_volumes;
/* 0x68 */ u32 mix_id;
/* 0x6C */ std::array<SplitterDestinationData::BiquadFilterParameter2, MaxBiquadFilters> biquad_filters;
/* 0x9C */ bool in_use;
/* 0x9D */ bool reset_prev_volume;
/* 0x9E */ u8 reserved[10];
};
static_assert(sizeof(InParameterVersion2b) == 0xA8,
"SplitterDestinationData::InParameterVersion2b has the wrong size!");
SplitterDestinationData(s32 id);
/**
@@ -116,6 +148,20 @@ public:
*/
void SetNext(SplitterDestinationData* next);
/**
* Get biquad filter parameters for this destination (REV15+).
*
* @return Span of biquad filter parameters.
*/
std::span<BiquadFilterParameter2> GetBiquadFilters();
/**
* Get const biquad filter parameters for this destination (REV15+).
*
* @return Const span of biquad filter parameters.
*/
std::span<const BiquadFilterParameter2> GetBiquadFilters() const;
private:
/// Id of this destination
const s32 id;
@@ -125,6 +171,8 @@ 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+)
std::array<BiquadFilterParameter2, MaxBiquadFilters> biquad_filters{};
/// Next destination in the mix chain
SplitterDestinationData* next{};
/// Is this destination in use?

View File

@@ -135,6 +135,17 @@ public:
static_assert(sizeof(BiquadFilterParameter) == 0xC,
"VoiceInfo::BiquadFilterParameter has the wrong size!");
struct BiquadFilterParameter2 {
/* 0x00 */ bool enabled;
/* 0x01 */ u8 reserved1;
/* 0x02 */ u8 reserved2;
/* 0x03 */ u8 reserved3;
/* 0x04 */ std::array<f32, 3> numerator; // b0, b1, b2
/* 0x10 */ std::array<f32, 2> denominator; // a1, a2 (a0 = 1)
};
static_assert(sizeof(BiquadFilterParameter2) == 0x18,
"VoiceInfo::BiquadFilterParameter2 has the wrong size!");
struct InParameter {
/* 0x000 */ u32 id;
/* 0x004 */ u32 node_id;
@@ -168,6 +179,43 @@ public:
};
static_assert(sizeof(InParameter) == 0x170, "VoiceInfo::InParameter has the wrong size!");
struct InParameter2 {
/* 0x000 */ u32 id;
/* 0x004 */ u32 node_id;
/* 0x008 */ bool is_new;
/* 0x009 */ bool in_use;
/* 0x00A */ PlayState play_state;
/* 0x00B */ SampleFormat sample_format;
/* 0x00C */ u32 sample_rate;
/* 0x010 */ s32 priority;
/* 0x014 */ s32 sort_order;
/* 0x018 */ u32 channel_count;
/* 0x01C */ f32 pitch;
/* 0x020 */ f32 volume;
/* 0x024 */ std::array<BiquadFilterParameter2, MaxBiquadFilters> biquads;
/* 0x054 */ u32 wave_buffer_count;
/* 0x058 */ u32 wave_buffer_index;
/* 0x05C */ u32 reserved1;
/* 0x060 */ CpuAddr src_data_address;
/* 0x068 */ u64 src_data_size;
/* 0x070 */ u32 mix_id;
/* 0x074 */ u32 splitter_id;
/* 0x078 */ std::array<WaveBufferInternal, MaxWaveBuffers> wave_buffer_internal;
/* 0x158 */ std::array<s32, MaxChannels> channel_resource_ids;
/* 0x170 */ bool clear_voice_drop;
/* 0x171 */ u8 flush_buffer_count;
/* 0x172 */ u16 reserved2;
/* 0x174 */ Flags flags;
/* 0x175 */ u8 reserved3;
/* 0x176 */ SrcQuality src_quality;
/* 0x177 */ u8 reserved4;
/* 0x178 */ u32 external_context;
/* 0x17C */ u32 external_context_size;
/* 0x180 */ u32 reserved5;
/* 0x184 */ u32 reserved6;
};
static_assert(sizeof(InParameter2) == 0x188, "VoiceInfo::InParameter2 has the wrong size!");
struct OutStatus {
/* 0x00 */ u64 played_sample_count;
/* 0x08 */ u32 wave_buffers_consumed;
@@ -349,6 +397,10 @@ public:
f32 prev_volume{};
/// Biquad filters for generating filter commands on this voice
std::array<BiquadFilterParameter, MaxBiquadFilters> biquads{};
/// Float biquad filters for REV15+ (native float coefficients)
std::array<BiquadFilterParameter2, MaxBiquadFilters> biquads_float{};
/// Use float biquad coefficients (REV15+)
bool use_float_biquads{};
/// Number of active wavebuffers
u32 wave_buffer_count{};
/// Current playing wavebuffer index