Merge branch 'sdk20-rev15-audio-changes' into 'main'

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

See merge request citron/emulator!97
This commit is contained in:
Zephyron
2025-10-11 17:11:09 +10:00
37 changed files with 2769 additions and 34 deletions

View File

@@ -30,15 +30,27 @@ add_library(audio_core STATIC
audio_out_manager.h audio_out_manager.h
audio_manager.cpp audio_manager.cpp
audio_manager.h audio_manager.h
audio_system_manager.cpp
audio_system_manager.h
audio_snoop_manager.cpp
audio_snoop_manager.h
common/audio_helpers.h
common/audio_renderer_parameter.h common/audio_renderer_parameter.h
common/common.h common/common.h
common/feature_support.h common/feature_support.h
common/fft.cpp
common/fft.h
common/loudness_calculator.cpp
common/loudness_calculator.h
common/wave_buffer.h common/wave_buffer.h
common/workbuffer_allocator.h common/workbuffer_allocator.h
device/audio_buffer.h device/audio_buffer.h
device/audio_buffer_list.h
device/audio_buffers.h device/audio_buffers.h
device/device_session.cpp device/device_session.cpp
device/device_session.h device/device_session.h
device/shared_ring_buffer.cpp
device/shared_ring_buffer.h
in/audio_in.cpp in/audio_in.cpp
in/audio_in.h in/audio_in.h
in/audio_in_system.cpp in/audio_in_system.cpp
@@ -85,6 +97,8 @@ add_library(audio_core STATIC
renderer/command/effect/i3dl2_reverb.h renderer/command/effect/i3dl2_reverb.h
renderer/command/effect/light_limiter.cpp renderer/command/effect/light_limiter.cpp
renderer/command/effect/light_limiter.h renderer/command/effect/light_limiter.h
renderer/command/effect/limiter.cpp
renderer/command/effect/limiter.h
renderer/command/effect/multi_tap_biquad_filter.cpp renderer/command/effect/multi_tap_biquad_filter.cpp
renderer/command/effect/multi_tap_biquad_filter.h renderer/command/effect/multi_tap_biquad_filter.h
renderer/command/effect/reverb.cpp renderer/command/effect/reverb.cpp
@@ -149,8 +163,13 @@ add_library(audio_core STATIC
renderer/effect/i3dl2.h renderer/effect/i3dl2.h
renderer/effect/light_limiter.cpp renderer/effect/light_limiter.cpp
renderer/effect/light_limiter.h renderer/effect/light_limiter.h
renderer/effect/limiter.cpp
renderer/effect/limiter.h
renderer/effect/reverb.h renderer/effect/reverb.h
renderer/effect/reverb.cpp renderer/effect/reverb.cpp
renderer/final_output_recorder/final_output_recorder_buffer.h
renderer/final_output_recorder/final_output_recorder_system.cpp
renderer/final_output_recorder/final_output_recorder_system.h
renderer/mix/mix_context.cpp renderer/mix/mix_context.cpp
renderer/mix/mix_context.h renderer/mix/mix_context.h
renderer/mix/mix_info.cpp renderer/mix/mix_info.cpp
@@ -218,6 +237,7 @@ if (MSVC)
/we4245 # 'conversion': conversion from 'type1' to 'type2', signed/unsigned mismatch /we4245 # 'conversion': conversion from 'type1' to 'type2', signed/unsigned mismatch
/we4254 # 'operator': conversion from 'type1:field_bits' to 'type2:field_bits', possible loss of data /we4254 # 'operator': conversion from 'type1:field_bits' to 'type2:field_bits', possible loss of data
/we4800 # Implicit conversion from 'type' to bool. Possible information loss /we4800 # Implicit conversion from 'type' to bool. Possible information loss
/wd2375 # Disable C2375: '__builtin_assume_aligned': redefinition (MSVC 14.44+ issue)
) )
else() else()
target_compile_options(audio_core PRIVATE target_compile_options(audio_core PRIVATE

View File

@@ -0,0 +1,78 @@
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/audio_snoop_manager.h"
#include "common/logging/log.h"
#include "core/core.h"
#include "core/hle/result.h"
namespace AudioCore {
AudioSnoopManager::AudioSnoopManager(Core::System& system_) : system{system_} {}
AudioSnoopManager::~AudioSnoopManager() = default;
Result AudioSnoopManager::GetDspStatistics(DspStatistics& statistics_out) {
std::scoped_lock lock{mutex};
if (!statistics_enabled) {
LOG_DEBUG(Service_Audio, "DSP statistics not enabled");
statistics_out = {};
return ResultSuccess;
}
statistics_out = statistics;
return ResultSuccess;
}
Result AudioSnoopManager::SetDspStatisticsParameter(bool enabled) {
std::scoped_lock lock{mutex};
LOG_DEBUG(Service_Audio, "Set DSP statistics enabled: {}", enabled);
statistics_enabled = enabled;
if (!enabled) {
// Clear statistics when disabled
statistics = {};
}
return ResultSuccess;
}
Result AudioSnoopManager::GetDspStatisticsParameter(bool& enabled) {
std::scoped_lock lock{mutex};
enabled = statistics_enabled;
return ResultSuccess;
}
Result AudioSnoopManager::GetAppletStateSummaries(std::span<AppletStateSummary> summaries,
u32& count) {
std::scoped_lock lock{mutex};
// For now, return empty summaries
// A full implementation would track active audio sessions per applet
count = 0;
return ResultSuccess;
}
void AudioSnoopManager::UpdateStatistics(u64 cycles_elapsed, u32 active_voices,
u32 dropped_commands) {
if (!statistics_enabled) {
return;
}
std::scoped_lock lock{mutex};
statistics.total_cycles += cycles_elapsed;
statistics.active_cycles += cycles_elapsed;
statistics.command_drop_count += dropped_commands;
// Calculate usage percentages (simplified)
if (statistics.total_cycles > 0) {
statistics.cpu_usage_percent =
static_cast<f32>(statistics.active_cycles) / static_cast<f32>(statistics.total_cycles) *
100.0f;
statistics.dsp_usage_percent = statistics.cpu_usage_percent * 0.5f; // Estimate
}
}
} // namespace AudioCore

View File

@@ -0,0 +1,100 @@
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <mutex>
#include <span>
#include "common/common_types.h"
#include "core/hle/result.h"
namespace Core {
class System;
}
namespace AudioCore {
/**
* Manages DSP statistics and performance monitoring.
*/
class AudioSnoopManager {
public:
struct DspStatistics {
/* 0x00 */ u64 total_cycles;
/* 0x08 */ u64 active_cycles;
/* 0x10 */ u32 voice_drop_count;
/* 0x14 */ u32 command_drop_count;
/* 0x18 */ u32 buffer_underrun_count;
/* 0x1C */ u32 buffer_overrun_count;
/* 0x20 */ f32 cpu_usage_percent;
/* 0x24 */ f32 dsp_usage_percent;
};
static_assert(sizeof(DspStatistics) == 0x28, "DspStatistics has the wrong size!");
struct AppletStateSummary {
/* 0x00 */ u64 applet_resource_user_id;
/* 0x08 */ u32 audio_in_active_count;
/* 0x0C */ u32 audio_out_active_count;
/* 0x10 */ u32 audio_renderer_active_count;
/* 0x14 */ u32 final_output_recorder_active_count;
/* 0x18 */ u32 total_active_count;
/* 0x1C */ u32 reserved;
};
static_assert(sizeof(AppletStateSummary) == 0x20, "AppletStateSummary has the wrong size!");
explicit AudioSnoopManager(Core::System& system);
~AudioSnoopManager();
/**
* Get DSP statistics.
*
* @param statistics - Output statistics structure.
* @return Result code.
*/
Result GetDspStatistics(DspStatistics& statistics);
/**
* Set DSP statistics parameter.
*
* @param enabled - Enable or disable statistics gathering.
* @return Result code.
*/
Result SetDspStatisticsParameter(bool enabled);
/**
* Get DSP statistics parameter.
*
* @param enabled - Output enabled state.
* @return Result code.
*/
Result GetDspStatisticsParameter(bool& enabled);
/**
* Get applet state summaries.
*
* @param summaries - Output array of summaries.
* @param count - Output count of summaries.
* @return Result code.
*/
Result GetAppletStateSummaries(std::span<AppletStateSummary> summaries, u32& count);
/**
* Update statistics (called periodically by the audio system).
*
* @param cycles_elapsed - Number of CPU cycles elapsed.
* @param active_voices - Number of active voices.
* @param dropped_commands - Number of dropped commands.
*/
void UpdateStatistics(u64 cycles_elapsed, u32 active_voices, u32 dropped_commands);
private:
Core::System& system;
std::mutex mutex;
DspStatistics statistics{};
bool statistics_enabled{false};
};
} // namespace AudioCore

View File

@@ -0,0 +1,128 @@
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include "audio_core/audio_system_manager.h"
#include "common/logging/log.h"
#include "core/core.h"
#include "core/hle/result.h"
#include "core/hle/service/audio/errors.h"
namespace AudioCore {
AudioSystemManager::AudioSystemManager(Core::System& system_) : system{system_} {}
AudioSystemManager::~AudioSystemManager() = default;
Result AudioSystemManager::RegisterAppletResourceUserId(u64 applet_resource_user_id) {
std::scoped_lock lock{mutex};
if (registered_count >= MaxAppletResourceUserIds) {
LOG_ERROR(Service_Audio, "Maximum applet resource user IDs registered");
return Service::Audio::ResultInvalidHandle;
}
// Check if already registered
for (size_t i = 0; i < registered_count; i++) {
if (registered_ids[i] == applet_resource_user_id) {
return ResultSuccess; // Already registered
}
}
registered_ids[registered_count++] = applet_resource_user_id;
LOG_DEBUG(Service_Audio, "Registered applet resource user ID: {}", applet_resource_user_id);
return ResultSuccess;
}
Result AudioSystemManager::UnregisterAppletResourceUserId(u64 applet_resource_user_id) {
std::scoped_lock lock{mutex};
for (size_t i = 0; i < registered_count; i++) {
if (registered_ids[i] == applet_resource_user_id) {
// Remove by shifting remaining elements
for (size_t j = i; j < registered_count - 1; j++) {
registered_ids[j] = registered_ids[j + 1];
}
registered_count--;
LOG_DEBUG(Service_Audio, "Unregistered applet resource user ID: {}",
applet_resource_user_id);
return ResultSuccess;
}
}
LOG_WARNING(Service_Audio, "Applet resource user ID not found: {}", applet_resource_user_id);
return Service::Audio::ResultInvalidHandle;
}
Result AudioSystemManager::RequestSuspendAudio(u64 applet_resource_user_id) {
std::scoped_lock lock{mutex};
LOG_DEBUG(Service_Audio, "Suspending audio for applet resource user ID: {}",
applet_resource_user_id);
audio_suspended = true;
return ResultSuccess;
}
Result AudioSystemManager::RequestResumeAudio(u64 applet_resource_user_id) {
std::scoped_lock lock{mutex};
LOG_DEBUG(Service_Audio, "Resuming audio for applet resource user ID: {}",
applet_resource_user_id);
audio_suspended = false;
return ResultSuccess;
}
Result AudioSystemManager::GetAudioInputProcessMasterVolume(f32& volume) {
std::scoped_lock lock{mutex};
volume = input_master_volume;
return ResultSuccess;
}
Result AudioSystemManager::SetAudioInputProcessMasterVolume(f32 volume) {
std::scoped_lock lock{mutex};
input_master_volume = std::clamp(volume, 0.0f, 1.0f);
LOG_DEBUG(Service_Audio, "Set audio input master volume: {}", input_master_volume);
return ResultSuccess;
}
Result AudioSystemManager::GetAudioOutputProcessMasterVolume(f32& volume) {
std::scoped_lock lock{mutex};
volume = output_master_volume;
return ResultSuccess;
}
Result AudioSystemManager::SetAudioOutputProcessMasterVolume(f32 volume) {
std::scoped_lock lock{mutex};
output_master_volume = std::clamp(volume, 0.0f, 1.0f);
LOG_DEBUG(Service_Audio, "Set audio output master volume: {}", output_master_volume);
return ResultSuccess;
}
Result AudioSystemManager::GetAudioOutputProcessRecordVolume(f32& volume) {
std::scoped_lock lock{mutex};
volume = output_record_volume;
return ResultSuccess;
}
Result AudioSystemManager::SetAudioOutputProcessRecordVolume(f32 volume) {
std::scoped_lock lock{mutex};
output_record_volume = std::clamp(volume, 0.0f, 1.0f);
LOG_DEBUG(Service_Audio, "Set audio output record volume: {}", output_record_volume);
return ResultSuccess;
}
Result AudioSystemManager::RequestSuspendAudioForDebug() {
std::scoped_lock lock{mutex};
LOG_DEBUG(Service_Audio, "Suspending audio for debug");
debug_suspended = true;
return ResultSuccess;
}
Result AudioSystemManager::RequestResumeAudioForDebug() {
std::scoped_lock lock{mutex};
LOG_DEBUG(Service_Audio, "Resuming audio for debug");
debug_suspended = false;
return ResultSuccess;
}
} // namespace AudioCore

View File

@@ -0,0 +1,137 @@
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <mutex>
#include "common/common_types.h"
#include "core/hle/result.h"
namespace Core {
class System;
}
namespace AudioCore {
/**
* Manages audio system state for applets including suspend/resume and volume control.
*/
class AudioSystemManager {
public:
explicit AudioSystemManager(Core::System& system);
~AudioSystemManager();
/**
* Register an applet resource user ID.
*
* @param applet_resource_user_id - Applet resource user ID to register.
* @return Result code.
*/
Result RegisterAppletResourceUserId(u64 applet_resource_user_id);
/**
* Unregister an applet resource user ID.
*
* @param applet_resource_user_id - Applet resource user ID to unregister.
* @return Result code.
*/
Result UnregisterAppletResourceUserId(u64 applet_resource_user_id);
/**
* Request audio suspension.
*
* @param applet_resource_user_id - Applet resource user ID.
* @return Result code.
*/
Result RequestSuspendAudio(u64 applet_resource_user_id);
/**
* Request audio resumption.
*
* @param applet_resource_user_id - Applet resource user ID.
* @return Result code.
*/
Result RequestResumeAudio(u64 applet_resource_user_id);
/**
* Get audio input process master volume.
*
* @param volume - Output volume value.
* @return Result code.
*/
Result GetAudioInputProcessMasterVolume(f32& volume);
/**
* Set audio input process master volume.
*
* @param volume - Volume value to set.
* @return Result code.
*/
Result SetAudioInputProcessMasterVolume(f32 volume);
/**
* Get audio output process master volume.
*
* @param volume - Output volume value.
* @return Result code.
*/
Result GetAudioOutputProcessMasterVolume(f32& volume);
/**
* Set audio output process master volume.
*
* @param volume - Volume value to set.
* @return Result code.
*/
Result SetAudioOutputProcessMasterVolume(f32 volume);
/**
* Get audio output process record volume.
*
* @param volume - Output volume value.
* @return Result code.
*/
Result GetAudioOutputProcessRecordVolume(f32& volume);
/**
* Set audio output process record volume.
*
* @param volume - Volume value to set.
* @return Result code.
*/
Result SetAudioOutputProcessRecordVolume(f32 volume);
/**
* Request audio suspension for debugging.
*
* @return Result code.
*/
Result RequestSuspendAudioForDebug();
/**
* Request audio resumption for debugging.
*
* @return Result code.
*/
Result RequestResumeAudioForDebug();
private:
static constexpr size_t MaxAppletResourceUserIds = 8;
Core::System& system;
std::mutex mutex;
std::array<u64, MaxAppletResourceUserIds> registered_ids{};
size_t registered_count{0};
f32 input_master_volume{1.0f};
f32 output_master_volume{1.0f};
f32 output_record_volume{1.0f};
bool audio_suspended{false};
bool debug_suspended{false};
};
} // namespace AudioCore

View File

@@ -0,0 +1,165 @@
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <span>
#include "audio_core/common/common.h"
#include "common/common_types.h"
namespace AudioCore {
/**
* ADPCM context structure
*/
struct AdpcmContext {
u16 header;
s16 yn0;
s16 yn1;
};
/**
* Parse ADPCM header information.
*
* @param context - Output context to receive parsed information.
* @param data - Input ADPCM data.
*/
inline void ParseAdpcmHeader(AdpcmContext& context, std::span<const u8> data) {
if (data.size() < 4) {
context = {};
return;
}
context.header = static_cast<u16>((data[0] << 8) | data[1]);
context.yn0 = static_cast<s16>((data[2] << 8) | data[3]);
context.yn1 = 0;
}
/**
* Get the byte size for a sample format.
*
* @param format - Sample format.
* @return Size in bytes.
*/
constexpr size_t GetSampleByteSize(SampleFormat format) {
return GetSampleFormatByteSize(format);
}
/**
* Initialize audio in parameter with defaults.
*
* @param params - Parameter structure to initialize.
*/
inline void InitializeAudioInParameter(auto& params) {
params.sample_rate = TargetSampleRate;
params.channel_count = 2;
params.reserved = 0;
}
/**
* Initialize audio out parameter with defaults.
*
* @param params - Parameter structure to initialize.
*/
inline void InitializeAudioOutParameter(auto& params) {
params.sample_rate = TargetSampleRate;
params.channel_count = 2;
params.reserved = 0;
}
/**
* Initialize final output recorder parameter with defaults.
*
* @param params - Parameter structure to initialize.
*/
inline void InitializeFinalOutputRecorderParameter(auto& params) {
params.sample_rate = TargetSampleRate;
params.channel_count = 2;
params.reserved = 0;
}
/**
* Set audio in buffer information.
*
* @param buffer - Buffer to set information for.
* @param data_address - Address of the sample data.
* @param size - Size of the buffer in bytes.
*/
inline void SetAudioInBufferInfo(auto& buffer, VAddr data_address, u64 size) {
buffer.samples = data_address;
buffer.capacity = size;
buffer.size = 0;
buffer.offset = 0;
}
/**
* Set audio out buffer information.
*
* @param buffer - Buffer to set information for.
* @param data_address - Address of the sample data.
* @param size - Size of the buffer in bytes.
*/
inline void SetAudioOutBufferInfo(auto& buffer, VAddr data_address, u64 size) {
buffer.samples = data_address;
buffer.capacity = size;
buffer.size = size;
buffer.offset = 0;
}
/**
* Get the data pointer from an audio buffer.
*
* @param buffer - Buffer to get data from.
* @return Address of the sample data.
*/
inline VAddr GetAudioBufferDataPointer(const auto& buffer) {
return buffer.samples;
}
/**
* Get the data size from an audio buffer.
*
* @param buffer - Buffer to get size from.
* @return Size of valid data in bytes.
*/
inline u64 GetAudioBufferDataSize(const auto& buffer) {
return buffer.size;
}
/**
* Get the buffer size from an audio buffer.
*
* @param buffer - Buffer to get size from.
* @return Total buffer capacity in bytes.
*/
inline u64 GetAudioBufferBufferSize(const auto& buffer) {
return buffer.capacity;
}
/**
* Get the required buffer size for final output recorder.
*
* @param sample_count - Number of samples.
* @param channel_count - Number of channels.
* @return Required buffer size in bytes.
*/
inline u64 GetFinalOutputRecorderWorkBufferSize(u32 sample_count, u32 channel_count) {
constexpr size_t buffer_header_size = 0x100;
const size_t sample_data_size = sample_count * channel_count * sizeof(s16);
const size_t buffer_count = 32;
return buffer_header_size + (sample_data_size * buffer_count);
}
/**
* Initialize final output recorder work buffer parameter.
*
* @param address - Work buffer address.
* @param size - Work buffer size.
*/
inline void InitializeFinalOutputRecorderWorkBufferParameter(VAddr address, u64 size) {
// Work buffer is just a memory region, no initialization needed
}
} // namespace AudioCore

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
@@ -13,7 +14,7 @@
#include "common/polyfill_ranges.h" #include "common/polyfill_ranges.h"
namespace AudioCore { namespace AudioCore {
constexpr u32 CurrentRevision = 13; constexpr u32 CurrentRevision = 15;
enum class SupportTags { enum class SupportTags {
CommandProcessingTimeEstimatorVersion4, CommandProcessingTimeEstimatorVersion4,
@@ -46,6 +47,8 @@ enum class SupportTags {
I3dl2ReverbChannelMappingChange, I3dl2ReverbChannelMappingChange,
CompressorStatistics, CompressorStatistics,
SplitterPrevVolumeReset, SplitterPrevVolumeReset,
SplitterDestinationV2b,
VoiceInParameterV2,
// Not a real tag, just here to get the count. // Not a real tag, just here to get the count.
Size Size
@@ -91,6 +94,9 @@ constexpr bool CheckFeatureSupported(SupportTags tag, u32 user_revision) {
{SupportTags::I3dl2ReverbChannelMappingChange, 11}, {SupportTags::I3dl2ReverbChannelMappingChange, 11},
{SupportTags::CompressorStatistics, 13}, {SupportTags::CompressorStatistics, 13},
{SupportTags::SplitterPrevVolumeReset, 13}, {SupportTags::SplitterPrevVolumeReset, 13},
{SupportTags::DeviceApiVersion2, 13},
{SupportTags::SplitterDestinationV2b, 15},
{SupportTags::VoiceInParameterV2, 15},
}}; }};
const auto& feature = const auto& feature =

View File

@@ -0,0 +1,167 @@
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <cmath>
#include <numbers>
#include "audio_core/common/fft.h"
#include "common/logging/log.h"
namespace AudioCore {
FFT::~FFT() = default;
size_t FFT::GetWorkBufferSize(u32 sample_count, ProcessMode mode) {
// Work buffer needs space for complex samples and temporary data
const size_t complex_size = sample_count * sizeof(std::complex<f32>);
const size_t temp_size = sample_count * sizeof(std::complex<f32>);
return complex_size + temp_size;
}
size_t FFT::GetWorkBufferAlignment() {
return 64; // Cache line alignment
}
bool FFT::Initialize(u32 sample_count, ProcessMode mode, void* work_buffer,
size_t work_buffer_size) {
// Verify sample count is power of 2
if ((sample_count & (sample_count - 1)) != 0) {
LOG_ERROR(Audio, "FFT sample count must be power of 2, got {}", sample_count);
return false;
}
if (work_buffer == nullptr || work_buffer_size < GetWorkBufferSize(sample_count, mode)) {
LOG_ERROR(Audio, "FFT work buffer too small");
return false;
}
sample_count_ = sample_count;
mode_ = mode;
initialized_ = true;
return true;
}
void FFT::ProcessRealToComplex(std::span<std::complex<f32>> output, std::span<const f32> input,
u32 sample_count) {
if (!initialized_ || sample_count != sample_count_) {
LOG_ERROR(Audio, "FFT not initialized or sample count mismatch");
return;
}
// Convert real input to complex
std::vector<std::complex<f32>> temp(sample_count);
for (u32 i = 0; i < sample_count; i++) {
temp[i] = std::complex<f32>(input[i], 0.0f);
}
// Perform FFT
FFTInternal(temp, sample_count, false);
// Copy to output (only first half + 1 due to symmetry)
const u32 output_count = sample_count / 2 + 1;
std::copy_n(temp.begin(), output_count, output.begin());
}
void FFT::ProcessComplexToReal(std::span<f32> output, std::span<const std::complex<f32>> input,
u32 sample_count) {
if (!initialized_ || sample_count != sample_count_) {
LOG_ERROR(Audio, "FFT not initialized or sample count mismatch");
return;
}
// Reconstruct full complex spectrum (conjugate symmetry)
std::vector<std::complex<f32>> temp(sample_count);
const u32 half = sample_count / 2;
// Copy provided values
for (u32 i = 0; i <= half; i++) {
temp[i] = input[i];
}
// Mirror with conjugate
for (u32 i = 1; i < half; i++) {
temp[sample_count - i] = std::conj(temp[i]);
}
// Perform inverse FFT
FFTInternal(temp, sample_count, true);
// Extract real part
for (u32 i = 0; i < sample_count; i++) {
output[i] = temp[i].real();
}
}
void FFT::ProcessComplexToComplex(std::span<std::complex<f32>> output,
std::span<const std::complex<f32>> input, u32 sample_count,
bool inverse) {
if (!initialized_ || sample_count != sample_count_) {
LOG_ERROR(Audio, "FFT not initialized or sample count mismatch");
return;
}
// Copy input to output
std::copy_n(input.begin(), sample_count, output.begin());
// Perform FFT
FFTInternal(output, sample_count, inverse);
}
void FFT::BitReverseCopy(std::span<std::complex<f32>> output,
std::span<const std::complex<f32>> input, u32 size) {
const u32 bits = std::bit_width(size - 1);
for (u32 i = 0; i < size; i++) {
u32 j = 0;
for (u32 b = 0; b < bits; b++) {
if (i & (1u << b)) {
j |= 1u << (bits - 1 - b);
}
}
output[j] = input[i];
}
}
void FFT::FFTInternal(std::span<std::complex<f32>> data, u32 size, bool inverse) {
// Cooley-Tukey FFT algorithm
std::vector<std::complex<f32>> temp(size);
BitReverseCopy(temp, data, size);
std::copy(temp.begin(), temp.end(), data.begin());
const f32 direction = inverse ? 1.0f : -1.0f;
const f32 scale = inverse ? (1.0f / size) : 1.0f;
// FFT stages
const u32 log2_size = static_cast<u32>(std::bit_width(size - 1u));
for (u32 s = 1; s <= log2_size; s++) {
const u32 m = 1u << s;
const u32 m2 = m / 2;
const std::complex<f32> wm = std::exp(std::complex<f32>(
0.0f, direction * 2.0f * std::numbers::pi_v<f32> / m));
for (u32 k = 0; k < size; k += m) {
std::complex<f32> w = 1.0f;
for (u32 j = 0; j < m2; j++) {
const std::complex<f32> t = w * data[k + j + m2];
const std::complex<f32> u = data[k + j];
data[k + j] = u + t;
data[k + j + m2] = u - t;
w *= wm;
}
}
}
// Apply scaling for inverse transform
if (inverse) {
for (u32 i = 0; i < size; i++) {
data[i] *= scale;
}
}
}
} // namespace AudioCore

View File

@@ -0,0 +1,99 @@
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <complex>
#include <memory>
#include <span>
#include "common/common_types.h"
namespace AudioCore {
/**
* FFT implementation for audio processing.
* Simplified version compatible with Nintendo SDK FFT functions.
*/
class FFT {
public:
enum class ProcessMode {
RealToComplex,
ComplexToReal,
ComplexToComplex,
};
FFT() = default;
~FFT();
/**
* Get the required work buffer size for FFT processing.
*
* @param sample_count - Number of samples to process (must be power of 2).
* @param mode - Processing mode.
* @return Required buffer size in bytes.
*/
static size_t GetWorkBufferSize(u32 sample_count, ProcessMode mode);
/**
* Get the required work buffer alignment.
*
* @return Required alignment in bytes.
*/
static size_t GetWorkBufferAlignment();
/**
* Initialize the FFT processor.
*
* @param sample_count - Number of samples to process (must be power of 2).
* @param mode - Processing mode.
* @param work_buffer - Work buffer for temporary data.
* @param work_buffer_size - Size of work buffer.
* @return True if initialization succeeded.
*/
bool Initialize(u32 sample_count, ProcessMode mode, void* work_buffer,
size_t work_buffer_size);
/**
* Process real-to-complex FFT (forward transform).
*
* @param output - Output complex samples.
* @param input - Input real samples.
* @param sample_count - Number of samples.
*/
void ProcessRealToComplex(std::span<std::complex<f32>> output, std::span<const f32> input,
u32 sample_count);
/**
* Process complex-to-real FFT (inverse transform).
*
* @param output - Output real samples.
* @param input - Input complex samples.
* @param sample_count - Number of samples.
*/
void ProcessComplexToReal(std::span<f32> output, std::span<const std::complex<f32>> input,
u32 sample_count);
/**
* Process complex-to-complex FFT.
*
* @param output - Output complex samples.
* @param input - Input complex samples.
* @param sample_count - Number of samples.
* @param inverse - True for inverse transform, false for forward.
*/
void ProcessComplexToComplex(std::span<std::complex<f32>> output,
std::span<const std::complex<f32>> input, u32 sample_count,
bool inverse);
private:
void BitReverseCopy(std::span<std::complex<f32>> output,
std::span<const std::complex<f32>> input, u32 size);
void FFTInternal(std::span<std::complex<f32>> data, u32 size, bool inverse);
u32 sample_count_{0};
ProcessMode mode_{ProcessMode::RealToComplex};
bool initialized_{false};
};
} // namespace AudioCore

View File

@@ -0,0 +1,208 @@
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <cmath>
#include <numbers>
#include "audio_core/common/loudness_calculator.h"
#include "common/logging/log.h"
namespace AudioCore {
LoudnessCalculator::LoudnessCalculator() = default;
LoudnessCalculator::~LoudnessCalculator() {
Finalize();
}
bool LoudnessCalculator::Initialize(const Parameters& params) {
if (params.channel_count == 0 || params.channel_count > MaxChannels) {
LOG_ERROR(Audio, "Invalid channel count: {}", params.channel_count);
return false;
}
if (params.sample_rate == 0) {
LOG_ERROR(Audio, "Invalid sample rate: {}", params.sample_rate);
return false;
}
params_ = params;
initialized_ = true;
InitializeKWeightingFilter();
Reset();
return true;
}
void LoudnessCalculator::Finalize() {
initialized_ = false;
}
void LoudnessCalculator::Reset() {
if (!initialized_) {
return;
}
momentary_loudness_ = -70.0f;
short_term_loudness_ = -70.0f;
integrated_loudness_ = -70.0f;
loudness_range_ = 0.0f;
integrated_sum_ = 0.0f;
integrated_count_ = 0;
buffer_index_ = 0;
momentary_buffer_.fill(0.0f);
short_term_buffer_.fill(0.0f);
// Reset filter state
std::fill(std::begin(k_filter_.z1_shelf), std::end(k_filter_.z1_shelf), 0.0f);
std::fill(std::begin(k_filter_.z2_shelf), std::end(k_filter_.z2_shelf), 0.0f);
std::fill(std::begin(k_filter_.z1_hp), std::end(k_filter_.z1_hp), 0.0f);
std::fill(std::begin(k_filter_.z2_hp), std::end(k_filter_.z2_hp), 0.0f);
}
void LoudnessCalculator::InitializeKWeightingFilter() {
// K-weighting filter coefficients for 48kHz
// Shelf filter (high-shelf +4dB at high frequencies)
const f32 f0_shelf = 1681.974450955533f;
const f32 Q_shelf = 0.7071752369554193f;
const f32 K_shelf = std::tan(std::numbers::pi_v<f32> * f0_shelf / params_.sample_rate);
const f32 Vh_shelf = std::pow(10.0f, 4.0f / 20.0f);
const f32 Vb_shelf = std::pow(Vh_shelf, 0.4996667741545416f);
const f32 a0_shelf = 1.0f + K_shelf / Q_shelf + K_shelf * K_shelf;
k_filter_.b0_shelf = (Vh_shelf + Vb_shelf * K_shelf / Q_shelf + K_shelf * K_shelf) / a0_shelf;
k_filter_.b1_shelf = 2.0f * (K_shelf * K_shelf - Vh_shelf) / a0_shelf;
k_filter_.b2_shelf = (Vh_shelf - Vb_shelf * K_shelf / Q_shelf + K_shelf * K_shelf) / a0_shelf;
k_filter_.a1_shelf = 2.0f * (K_shelf * K_shelf - 1.0f) / a0_shelf;
k_filter_.a2_shelf = (1.0f - K_shelf / Q_shelf + K_shelf * K_shelf) / a0_shelf;
// High-pass filter (48Hz cutoff)
const f32 f0_hp = 38.13547087602444f;
const f32 Q_hp = 0.5003270373238773f;
const f32 K_hp = std::tan(std::numbers::pi_v<f32> * f0_hp / params_.sample_rate);
const f32 a0_hp = 1.0f + K_hp / Q_hp + K_hp * K_hp;
k_filter_.b0_hp = 1.0f / a0_hp;
k_filter_.b1_hp = -2.0f / a0_hp;
k_filter_.b2_hp = 1.0f / a0_hp;
k_filter_.a1_hp = 2.0f * (K_hp * K_hp - 1.0f) / a0_hp;
k_filter_.a2_hp = (1.0f - K_hp / Q_hp + K_hp * K_hp) / a0_hp;
}
f32 LoudnessCalculator::ApplyKWeighting(f32 sample, u32 channel) {
// Apply shelf filter
const f32 out_shelf = k_filter_.b0_shelf * sample + k_filter_.b1_shelf * k_filter_.z1_shelf[channel] +
k_filter_.b2_shelf * k_filter_.z2_shelf[channel] -
k_filter_.a1_shelf * k_filter_.z1_shelf[channel] -
k_filter_.a2_shelf * k_filter_.z2_shelf[channel];
k_filter_.z2_shelf[channel] = k_filter_.z1_shelf[channel];
k_filter_.z1_shelf[channel] = sample;
// Apply high-pass filter
const f32 out_hp = k_filter_.b0_hp * out_shelf + k_filter_.b1_hp * k_filter_.z1_hp[channel] +
k_filter_.b2_hp * k_filter_.z2_hp[channel] -
k_filter_.a1_hp * k_filter_.z1_hp[channel] -
k_filter_.a2_hp * k_filter_.z2_hp[channel];
k_filter_.z2_hp[channel] = k_filter_.z1_hp[channel];
k_filter_.z1_hp[channel] = out_shelf;
return out_hp;
}
void LoudnessCalculator::Analyze(std::span<const f32> samples, u32 sample_count) {
if (!initialized_) {
return;
}
const u32 total_samples = sample_count * params_.channel_count;
if (samples.size() < total_samples) {
return;
}
// Process samples
for (u32 i = 0; i < sample_count; i++) {
f32 sum_square = 0.0f;
// Apply K-weighting and calculate mean square for each channel
for (u32 ch = 0; ch < params_.channel_count; ch++) {
const f32 sample = samples[i * params_.channel_count + ch];
const f32 weighted = ApplyKWeighting(sample, ch);
// Channel weighting (LFE channel has less weight)
const f32 weight = (ch == 3) ? 0.0f : 1.0f; // Channel 3 is typically LFE
sum_square += weight * weighted * weighted;
}
// Calculate mean square
const f32 mean_square = sum_square / params_.channel_count;
// Update buffers
momentary_buffer_[buffer_index_ % momentary_buffer_.size()] = mean_square;
short_term_buffer_[buffer_index_ % short_term_buffer_.size()] = mean_square;
buffer_index_++;
// Update integrated measurement
integrated_sum_ += mean_square;
integrated_count_++;
}
// Calculate momentary loudness (last 400ms)
const size_t momentary_samples = std::min(buffer_index_,
static_cast<size_t>(params_.sample_rate * 0.4f));
f32 momentary_sum = 0.0f;
for (size_t i = 0; i < momentary_samples; i++) {
momentary_sum += momentary_buffer_[i];
}
momentary_loudness_ = CalculateLoudness(momentary_sum / momentary_samples);
// Calculate short-term loudness (last 3s)
const size_t short_term_samples = std::min(buffer_index_,
static_cast<size_t>(params_.sample_rate * 3.0f));
f32 short_term_sum = 0.0f;
for (size_t i = 0; i < short_term_samples; i++) {
short_term_sum += short_term_buffer_[i];
}
short_term_loudness_ = CalculateLoudness(short_term_sum / short_term_samples);
// Calculate integrated loudness
if (integrated_count_ > 0) {
integrated_loudness_ = CalculateLoudness(integrated_sum_ / integrated_count_);
}
}
f32 LoudnessCalculator::CalculateLoudness(f32 mean_square) {
if (mean_square <= 0.0f) {
return -70.0f; // Silence
}
// Convert to LUFS: -0.691 + 10*log10(mean_square)
return -0.691f + 10.0f * std::log10(mean_square);
}
f32 LoudnessCalculator::GetMomentaryLoudness() const {
return momentary_loudness_;
}
f32 LoudnessCalculator::GetShortTermLoudness() const {
return short_term_loudness_;
}
f32 LoudnessCalculator::GetIntegratedLoudness() const {
return integrated_loudness_;
}
f32 LoudnessCalculator::GetLoudnessRange() const {
return loudness_range_;
}
void LoudnessCalculator::SetMomentaryLoudnessLpfTc(f32 time_constant) {
if (initialized_) {
params_.momentary_time_constant = time_constant;
}
}
} // namespace AudioCore

View File

@@ -0,0 +1,131 @@
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <memory>
#include <span>
#include "common/common_types.h"
namespace AudioCore {
/**
* Loudness calculator following ITU-R BS.1770 standard.
* Used for measuring audio loudness in LUFS (Loudness Units Full Scale).
*/
class LoudnessCalculator {
public:
static constexpr size_t MaxChannels = 6;
struct Parameters {
u32 sample_rate;
u32 channel_count;
f32 momentary_time_constant; // Default: 0.4s
f32 short_term_time_constant; // Default: 3.0s
};
LoudnessCalculator();
~LoudnessCalculator();
/**
* Initialize the loudness calculator.
*
* @param params - Configuration parameters.
* @return True if initialization succeeded.
*/
bool Initialize(const Parameters& params);
/**
* Finalize and cleanup the calculator.
*/
void Finalize();
/**
* Reset the calculator state.
*/
void Reset();
/**
* Analyze a block of audio samples.
*
* @param samples - Interleaved audio samples.
* @param sample_count - Number of samples per channel.
*/
void Analyze(std::span<const f32> samples, u32 sample_count);
/**
* Get momentary loudness (400ms window).
*
* @return Loudness in LUFS.
*/
f32 GetMomentaryLoudness() const;
/**
* Get short-term loudness (3s window).
*
* @return Loudness in LUFS.
*/
f32 GetShortTermLoudness() const;
/**
* Get integrated loudness (entire duration).
*
* @return Loudness in LUFS.
*/
f32 GetIntegratedLoudness() const;
/**
* Get loudness range.
*
* @return Loudness range in LU.
*/
f32 GetLoudnessRange() const;
/**
* Set momentary loudness low-pass filter time constant.
*
* @param time_constant - Time constant in seconds.
*/
void SetMomentaryLoudnessLpfTc(f32 time_constant);
private:
struct KWeightingFilter {
// Shelf filter coefficients
f32 b0_shelf, b1_shelf, b2_shelf;
f32 a1_shelf, a2_shelf;
f32 z1_shelf[MaxChannels], z2_shelf[MaxChannels];
// High-pass filter coefficients
f32 b0_hp, b1_hp, b2_hp;
f32 a1_hp, a2_hp;
f32 z1_hp[MaxChannels], z2_hp[MaxChannels];
};
void InitializeKWeightingFilter();
f32 ApplyKWeighting(f32 sample, u32 channel);
f32 CalculateLoudness(f32 mean_square);
Parameters params_{};
bool initialized_{false};
KWeightingFilter k_filter_{};
// Loudness measurements
f32 momentary_loudness_{-70.0f};
f32 short_term_loudness_{-70.0f};
f32 integrated_loudness_{-70.0f};
f32 loudness_range_{0.0f};
// Integrated measurement state
f32 integrated_sum_{0.0f};
u64 integrated_count_{0};
// Ring buffers for time-windowed measurements
std::array<f32, 48000> momentary_buffer_{}; // 1 second at 48kHz
std::array<f32, 144000> short_term_buffer_{}; // 3 seconds at 48kHz
size_t buffer_index_{0};
};
} // namespace AudioCore

View File

@@ -0,0 +1,93 @@
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <span>
#include "common/common_types.h"
namespace AudioCore {
template <typename BufferType>
class AudioBufferList {
public:
static constexpr size_t BufferCount = 32;
AudioBufferList() = default;
~AudioBufferList() = default;
void clear() {
count = 0;
head_index = 0;
tail_index = 0;
}
void push_back(const BufferType& buffer) {
if (count >= BufferCount) {
return;
}
buffers[tail_index] = buffer;
tail_index = (tail_index + 1) % BufferCount;
count++;
}
void pop_front() {
if (count == 0) {
return;
}
head_index = (head_index + 1) % BufferCount;
count--;
}
BufferType& front() {
return buffers[head_index];
}
const BufferType& front() const {
return buffers[head_index];
}
BufferType& back() {
size_t index = (tail_index + BufferCount - 1) % BufferCount;
return buffers[index];
}
const BufferType& back() const {
size_t index = (tail_index + BufferCount - 1) % BufferCount;
return buffers[index];
}
size_t size() const {
return count;
}
bool empty() const {
return count == 0;
}
bool full() const {
return count >= BufferCount;
}
bool contains(const BufferType* buffer_ptr) const {
for (size_t i = 0; i < count; i++) {
size_t index = (head_index + i) % BufferCount;
if (&buffers[index] == buffer_ptr) {
return true;
}
}
return false;
}
private:
std::array<BufferType, BufferCount> buffers{};
size_t count{0};
size_t head_index{0};
size_t tail_index{0};
};
} // namespace AudioCore

View File

@@ -0,0 +1,169 @@
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/device/shared_ring_buffer.h"
#include "audio_core/renderer/final_output_recorder/final_output_recorder_buffer.h"
#include "common/logging/log.h"
#include "core/memory.h"
namespace AudioCore {
SharedRingBuffer::SharedRingBuffer() = default;
SharedRingBuffer::~SharedRingBuffer() {
Finalize();
}
bool SharedRingBuffer::Initialize(Core::Memory::Memory& memory_, VAddr buffer_address_,
u64 buffer_size_, VAddr data_address_, u64 data_size_,
u32 buffer_count) {
if (initialized) {
return false;
}
memory = std::addressof(memory_);
buffer_address = buffer_address_;
buffer_size = buffer_size_;
data_address = data_address_;
data_size = data_size_;
max_buffer_count = buffer_count;
current_buffer_count = 0;
read_offset = 0;
write_offset = 0;
initialized = true;
ClearSharedState();
return true;
}
void SharedRingBuffer::Finalize() {
if (!initialized) {
return;
}
initialized = false;
memory = static_cast<Core::Memory::Memory*>(nullptr);
buffer_address = 0;
buffer_size = 0;
data_address = 0;
data_size = 0;
max_buffer_count = 0;
current_buffer_count = 0;
read_offset = 0;
write_offset = 0;
}
void SharedRingBuffer::ClearSharedState() {
if (!initialized || !memory) {
return;
}
SharedState state{};
state.read_offset = 0;
state.write_offset = 0;
state.buffer_count = 0;
state.sample_rate = 48000;
state.channel_count = 2;
state.sample_format = 2; // PCM16
memory->WriteBlock(data_address, &state, sizeof(SharedState));
}
bool SharedRingBuffer::GetReleasedBufferForPlayback(FinalOutputRecorderBuffer& out_buffer) {
// Not implemented for playback
return false;
}
bool SharedRingBuffer::GetReleasedBufferForRecord(FinalOutputRecorderBuffer& out_buffer) {
if (!initialized || !memory || current_buffer_count == 0) {
return false;
}
// Read the buffer info from the ring
memory->ReadBlock(buffer_address + read_offset, &out_buffer, sizeof(FinalOutputRecorderBuffer));
read_offset += sizeof(FinalOutputRecorderBuffer);
if (read_offset >= buffer_size) {
read_offset = 0;
}
current_buffer_count--;
return true;
}
bool SharedRingBuffer::AppendBufferForPlayback(const FinalOutputRecorderBuffer& buffer) {
// Not implemented for playback
return false;
}
bool SharedRingBuffer::HasCapacityForAppend() const {
return initialized && current_buffer_count < max_buffer_count;
}
bool SharedRingBuffer::HasAvailableBuffer() const {
return initialized && current_buffer_count > 0;
}
bool SharedRingBuffer::AppendBufferForRecord(const FinalOutputRecorderBuffer& buffer) {
if (!initialized || !memory || !HasCapacityForRecord()) {
return false;
}
// Write the buffer info to the ring
memory->WriteBlock(buffer_address + write_offset, &buffer,
sizeof(FinalOutputRecorderBuffer));
write_offset += sizeof(FinalOutputRecorderBuffer);
if (write_offset >= buffer_size) {
write_offset = 0;
}
current_buffer_count++;
return true;
}
bool SharedRingBuffer::HasCapacityForRecord() const {
return initialized && current_buffer_count < max_buffer_count;
}
bool SharedRingBuffer::ContainsBuffer(VAddr buffer_address_) const {
if (!initialized || !memory) {
return false;
}
u64 temp_offset = read_offset;
for (u32 i = 0; i < current_buffer_count; i++) {
FinalOutputRecorderBuffer buffer;
memory->ReadBlock(buffer_address + temp_offset, &buffer,
sizeof(FinalOutputRecorderBuffer));
if (buffer.samples == buffer_address_) {
return true;
}
temp_offset += sizeof(FinalOutputRecorderBuffer);
if (temp_offset >= buffer_size) {
temp_offset = 0;
}
}
return false;
}
u32 SharedRingBuffer::GetBufferCount() const {
return current_buffer_count;
}
u64 SharedRingBuffer::GetSampleProcessedSampleCount() const {
// Return the total number of samples processed
return 0; // TODO: Track this
}
u64 SharedRingBuffer::GetWorkBufferDataSizeBytes() const {
return buffer_size;
}
VAddr SharedRingBuffer::GetWorkBufferDataAddress() const {
return buffer_address;
}
} // namespace AudioCore

View File

@@ -0,0 +1,167 @@
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <memory>
#include <span>
#include "common/common_types.h"
namespace Core::Memory {
class Memory;
}
namespace AudioCore {
struct FinalOutputRecorderBuffer;
/**
* Shared ring buffer for final output recording.
* Manages a circular buffer shared between the emulator and the game.
*/
class SharedRingBuffer {
public:
struct SharedState {
u64 read_offset;
u64 write_offset;
u32 buffer_count;
u32 sample_rate;
u32 channel_count;
u32 sample_format;
};
SharedRingBuffer();
~SharedRingBuffer();
// Prevent copying
SharedRingBuffer(const SharedRingBuffer&) = delete;
SharedRingBuffer& operator=(const SharedRingBuffer&) = delete;
/**
* Initialize the shared ring buffer.
*
* @param memory - Core memory for accessing shared memory.
* @param buffer_address - Address of the ring buffer data.
* @param buffer_size - Size of the ring buffer in bytes.
* @param data_address - Address of the shared state.
* @param data_size - Size of the shared state.
* @param buffer_count - Maximum number of buffers that can be queued.
* @return True if initialization succeeded.
*/
bool Initialize(Core::Memory::Memory& memory, VAddr buffer_address, u64 buffer_size,
VAddr data_address, u64 data_size, u32 buffer_count);
/**
* Finalize and cleanup the ring buffer.
*/
void Finalize();
/**
* Clear the shared state.
*/
void ClearSharedState();
/**
* Get a released buffer for playback.
*
* @param out_buffer - Output buffer information.
* @return True if a buffer was available.
*/
bool GetReleasedBufferForPlayback(FinalOutputRecorderBuffer& out_buffer);
/**
* Get a released buffer for recording.
*
* @param out_buffer - Output buffer information.
* @return True if a buffer was available.
*/
bool GetReleasedBufferForRecord(FinalOutputRecorderBuffer& out_buffer);
/**
* Append a buffer for playback.
*
* @param buffer - Buffer to append.
* @return True if the buffer was successfully appended.
*/
bool AppendBufferForPlayback(const FinalOutputRecorderBuffer& buffer);
/**
* Check if there's capacity to append a new buffer.
*
* @return True if there's capacity.
*/
bool HasCapacityForAppend() const;
/**
* Check if there's an available buffer to retrieve.
*
* @return True if a buffer is available.
*/
bool HasAvailableBuffer() const;
/**
* Append a buffer for recording.
*
* @param buffer - Buffer to append.
* @return True if the buffer was successfully appended.
*/
bool AppendBufferForRecord(const FinalOutputRecorderBuffer& buffer);
/**
* Check if there's capacity to record a new buffer.
*
* @return True if there's capacity.
*/
bool HasCapacityForRecord() const;
/**
* Check if the ring buffer contains a specific buffer.
*
* @param buffer_address - Address of the buffer to check.
* @return True if the buffer is in the ring.
*/
bool ContainsBuffer(VAddr buffer_address) const;
/**
* Get the current buffer count.
*
* @return Number of buffers currently in the ring.
*/
u32 GetBufferCount() const;
/**
* Get the sample processed sample count.
*
* @return Number of samples processed.
*/
u64 GetSampleProcessedSampleCount() const;
/**
* Get the work buffer data size in bytes.
*
* @return Size of the work buffer data.
*/
u64 GetWorkBufferDataSizeBytes() const;
/**
* Get the work buffer data address.
*
* @return Address of the work buffer data.
*/
VAddr GetWorkBufferDataAddress() const;
private:
Core::Memory::Memory* memory{nullptr};
VAddr buffer_address{0};
u64 buffer_size{0};
VAddr data_address{0};
u64 data_size{0};
u32 max_buffer_count{0};
u32 current_buffer_count{0};
u64 read_offset{0};
u64 write_offset{0};
bool initialized{false};
};
} // namespace AudioCore

View File

@@ -206,6 +206,16 @@ void System::SetVolume(const f32 volume_) {
session->SetVolume(volume_); session->SetVolume(volume_);
} }
f32 System::GetDeviceGain() const {
return device_gain;
}
void System::SetDeviceGain(const f32 gain) {
device_gain = gain;
// Apply the device gain to the session
session->SetVolume(volume * device_gain);
}
bool System::ContainsAudioBuffer(const u64 tag) const { bool System::ContainsAudioBuffer(const u64 tag) const {
return buffers.ContainsBuffer(tag); return buffers.ContainsBuffer(tag);
} }

View File

@@ -213,6 +213,20 @@ public:
*/ */
void SetVolume(f32 volume); void SetVolume(f32 volume);
/**
* Get this system's current device gain.
*
* @return The system's current device gain.
*/
f32 GetDeviceGain() const;
/**
* Set this system's current device gain.
*
* @param gain The new device gain.
*/
void SetDeviceGain(f32 gain);
/** /**
* Does the system contain this buffer? * Does the system contain this buffer?
* *
@@ -269,6 +283,8 @@ private:
std::string name{}; std::string name{};
/// Volume of this system /// Volume of this system
f32 volume{1.0f}; f32 volume{1.0f};
/// Device gain of this system
f32 device_gain{1.0f};
/// Is this system's device USB? /// Is this system's device USB?
bool is_uac{false}; bool is_uac{false};
}; };

View File

@@ -198,4 +198,12 @@ bool BehaviorInfo::IsSplitterPrevVolumeResetSupported() const {
return CheckFeatureSupported(SupportTags::SplitterPrevVolumeReset, user_revision); 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 } // namespace AudioCore::Renderer

View File

@@ -378,6 +378,22 @@ public:
*/ */
bool IsSplitterPrevVolumeResetSupported() const; 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 /// Host version
u32 process_revision; u32 process_revision;
/// User version /// 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, const PoolMapper pool_mapper(process_handle, memory_pools, memory_pool_count,
behaviour.IsMemoryForceMappingEnabled()); behaviour.IsMemoryForceMappingEnabled());
const auto voice_count{voice_context.GetCount()}; 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), std::span<VoiceInfo::OutStatus> out_params{reinterpret_cast<VoiceInfo::OutStatus*>(output),
voice_count}; voice_count};
@@ -73,8 +71,97 @@ Result InfoUpdater::UpdateVoices(VoiceContext& voice_context,
u32 new_voice_count{0}; 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++) { 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{}; std::array<VoiceState*, MaxChannels> voice_states{};
if (!in_param.in_use) { if (!in_param.in_use) {
@@ -98,6 +185,14 @@ Result InfoUpdater::UpdateVoices(VoiceContext& voice_context,
BehaviorInfo::ErrorInfo update_error{}; BehaviorInfo::ErrorInfo update_error{};
voice_info.UpdateParameters(update_error, in_param, pool_mapper, behaviour); 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()) { if (!update_error.error_code.IsSuccess()) {
behaviour.AppendError(update_error); behaviour.AppendError(update_error);
} }
@@ -118,7 +213,7 @@ Result InfoUpdater::UpdateVoices(VoiceContext& voice_context,
new_voice_count += in_param.channel_count; 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))}; auto consumed_output_size{voice_count * static_cast<u32>(sizeof(VoiceInfo::OutStatus))};
if (consumed_input_size != in_header->voices_size) { if (consumed_input_size != in_header->voices_size) {
LOG_ERROR(Service_Audio, "Consumed an incorrect voices size, header size={}, consumed={}", 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) { EffectContext& effect_context, SplitterContext& splitter_context) {
s32 mix_count{0}; s32 mix_count{0};
u32 consumed_input_size{0}; u32 consumed_input_size{0};
u32 input_mix_size{0};
if (behaviour.IsMixInParameterDirtyOnlyUpdateSupported()) { if (behaviour.IsMixInParameterDirtyOnlyUpdateSupported()) {
auto in_dirty_params{reinterpret_cast<const MixInfo::InDirtyParameter*>(input)}; auto in_dirty_params{reinterpret_cast<const MixInfo::InDirtyParameter*>(input)};
mix_count = in_dirty_params->count; 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); input += sizeof(MixInfo::InDirtyParameter);
consumed_input_size = static_cast<u32>(sizeof(MixInfo::InDirtyParameter) +
mix_count * sizeof(MixInfo::InParameter));
} else { } else {
mix_count = mix_context.GetCount(); 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) { if (mix_buffer_count == 0) {
return Service::Audio::ResultInvalidUpdateInfo; return Service::Audio::ResultInvalidUpdateInfo;
} }
@@ -330,7 +436,7 @@ Result InfoUpdater::UpdateMixes(MixContext& mix_context, const u32 mix_buffer_co
return Service::Audio::ResultInvalidUpdateInfo; return Service::Audio::ResultInvalidUpdateInfo;
} }
input += mix_count * sizeof(MixInfo::InParameter); input += input_mix_size;
return ResultSuccess; return ResultSuccess;
} }

View File

@@ -234,6 +234,14 @@ void CommandBuffer::GenerateBiquadFilterCommand(const s32 node_id, VoiceInfo& vo
cmd.biquad = voice_info.biquads[biquad_index]; 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()), cmd.state = memory_pool->Translate(CpuAddr(voice_state.biquad_states[biquad_index].data()),
MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState)); MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState));
@@ -260,6 +268,9 @@ void CommandBuffer::GenerateBiquadFilterCommand(const s32 node_id, EffectInfoBas
cmd.biquad.b = parameter.b; cmd.biquad.b = parameter.b;
cmd.biquad.a = parameter.a; cmd.biquad.a = parameter.a;
// Effects use legacy fixed-point format
cmd.use_float_coefficients = false;
cmd.state = memory_pool->Translate(CpuAddr(state), cmd.state = memory_pool->Translate(CpuAddr(state),
MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState)); MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState));
@@ -655,6 +666,14 @@ void CommandBuffer::GenerateMultitapBiquadFilterCommand(const s32 node_id, Voice
cmd.output = buffer_count + channel; cmd.output = buffer_count + channel;
cmd.biquads = voice_info.biquads; 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] = cmd.states[0] =
memory_pool->Translate(CpuAddr(voice_state.biquad_states[0].data()), memory_pool->Translate(CpuAddr(voice_state.biquad_states[0].data()),
MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState)); 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]); 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. * 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)}; processor.mix_buffers.subspan(output * processor.sample_count, processor.sample_count)};
if (use_float_processing) { if (use_float_processing) {
ApplyBiquadFilterFloat(output_buffer, input_buffer, biquad.b, biquad.a, *state_, // REV15+: Use native float coefficients if available
processor.sample_count); 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 { } else {
ApplyBiquadFilterInt(output_buffer, input_buffer, biquad.b, biquad.a, *state_, ApplyBiquadFilterInt(output_buffer, input_buffer, biquad.b, biquad.a, *state_,
processor.sample_count); processor.sample_count);

View File

@@ -48,14 +48,18 @@ struct BiquadFilterCommand : ICommand {
s16 input; s16 input;
/// Output mix buffer index /// Output mix buffer index
s16 output; s16 output;
/// Input parameters for biquad /// Input parameters for biquad (legacy fixed-point)
VoiceInfo::BiquadFilterParameter biquad; VoiceInfo::BiquadFilterParameter biquad;
/// Input parameters for biquad (REV15+ native float)
VoiceInfo::BiquadFilterParameter2 biquad_float;
/// Biquad state, updated each call /// Biquad state, updated each call
CpuAddr state; CpuAddr state;
/// If true, reset the state /// If true, reset the state
bool needs_init; bool needs_init;
/// If true, use float processing rather than int /// If true, use float processing rather than int
bool use_float_processing; 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, std::array<s16, 3>& b, std::array<s16, 2>& a,
VoiceState::BiquadFilterState& state, const u32 sample_count); 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 } // 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 = {}; *state = {};
} }
ApplyBiquadFilterFloat(output_buffer, input_buffer, biquads[i].b, biquads[i].a, *state, // REV15+: Use native float coefficients if available
processor.sample_count); 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; s16 input;
/// Output mix buffer index /// Output mix buffer index
s16 output; s16 output;
/// Biquad parameters /// Biquad parameters (legacy fixed-point)
std::array<VoiceInfo::BiquadFilterParameter, MaxBiquadFilters> biquads; std::array<VoiceInfo::BiquadFilterParameter, MaxBiquadFilters> biquads;
/// Biquad parameters (REV15+ native float)
std::array<VoiceInfo::BiquadFilterParameter2, MaxBiquadFilters> biquads_float;
/// Biquad states, updated each call /// Biquad states, updated each call
std::array<CpuAddr, MaxBiquadFilters> states; std::array<CpuAddr, MaxBiquadFilters> states;
/// If each biquad needs initialisation /// If each biquad needs initialisation
std::array<bool, MaxBiquadFilters> needs_init; std::array<bool, MaxBiquadFilters> needs_init;
/// Number of active biquads /// Number of active biquads
u8 filter_tap_count; u8 filter_tap_count;
/// If true, use native float coefficients (REV15+)
bool use_float_coefficients;
}; };
} // namespace AudioCore::Renderer } // 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_; destinations_count = destination_count_;
splitter_bug_fixed = splitter_bug_fixed_; splitter_bug_fixed = splitter_bug_fixed_;
splitter_prev_volume_reset_supported = behavior.IsSplitterPrevVolumeResetSupported(); splitter_prev_volume_reset_supported = behavior.IsSplitterPrevVolumeResetSupported();
splitter_float_coeff_supported = behavior.IsSplitterDestinationV2bSupported();
} }
bool SplitterContext::UsingSplitter() const { 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) { u32 SplitterContext::UpdateData(const u8* input, u32 offset, const u32 count) {
for (u32 i = 0; i < count; i++) { for (u32 i = 0; i < count; i++) {
auto data_header{ // Version selection based on float coeff/biquad v2b support.
reinterpret_cast<const SplitterDestinationData::InParameter*>(input + offset)}; if (!splitter_float_coeff_supported) {
const auto* data_header =
reinterpret_cast<const SplitterDestinationData::InParameter*>(input + offset);
if (data_header->magic != GetSplitterSendDataMagic()) { if (data_header->magic != GetSplitterSendDataMagic()) {
continue; 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; return offset;

View File

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

View File

@@ -87,4 +87,12 @@ void SplitterDestinationData::SetNext(SplitterDestinationData* next_) {
next = 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 } // namespace AudioCore::Renderer

View File

@@ -10,12 +10,31 @@
#include "common/common_types.h" #include "common/common_types.h"
namespace AudioCore::Renderer { namespace AudioCore::Renderer {
// Forward declaration
class VoiceInfo;
/** /**
* Represents a mixing node, can be connected to a previous and next destination forming a chain * 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. * that a certain mix buffer will pass through to output.
*/ */
class SplitterDestinationData { class SplitterDestinationData {
public: 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 { struct InParameter {
/* 0x00 */ u32 magic; // 'SNDD' /* 0x00 */ u32 magic; // 'SNDD'
/* 0x04 */ s32 id; /* 0x04 */ s32 id;
@@ -27,6 +46,19 @@ public:
static_assert(sizeof(InParameter) == 0x70, static_assert(sizeof(InParameter) == 0x70,
"SplitterDestinationData::InParameter has the wrong size!"); "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); SplitterDestinationData(s32 id);
/** /**
@@ -116,6 +148,20 @@ public:
*/ */
void SetNext(SplitterDestinationData* next); 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: private:
/// Id of this destination /// Id of this destination
const s32 id; const s32 id;
@@ -125,6 +171,8 @@ private:
std::array<f32, MaxMixBuffers> mix_volumes{0.0f}; std::array<f32, MaxMixBuffers> mix_volumes{0.0f};
/// Previous mix volumes /// Previous mix volumes
std::array<f32, MaxMixBuffers> prev_mix_volumes{0.0f}; 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 /// Next destination in the mix chain
SplitterDestinationData* next{}; SplitterDestinationData* next{};
/// Is this destination in use? /// Is this destination in use?

View File

@@ -135,6 +135,17 @@ public:
static_assert(sizeof(BiquadFilterParameter) == 0xC, static_assert(sizeof(BiquadFilterParameter) == 0xC,
"VoiceInfo::BiquadFilterParameter has the wrong size!"); "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 { struct InParameter {
/* 0x000 */ u32 id; /* 0x000 */ u32 id;
/* 0x004 */ u32 node_id; /* 0x004 */ u32 node_id;
@@ -168,6 +179,43 @@ public:
}; };
static_assert(sizeof(InParameter) == 0x170, "VoiceInfo::InParameter has the wrong size!"); 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 { struct OutStatus {
/* 0x00 */ u64 played_sample_count; /* 0x00 */ u64 played_sample_count;
/* 0x08 */ u32 wave_buffers_consumed; /* 0x08 */ u32 wave_buffers_consumed;
@@ -349,6 +397,10 @@ public:
f32 prev_volume{}; f32 prev_volume{};
/// Biquad filters for generating filter commands on this voice /// Biquad filters for generating filter commands on this voice
std::array<BiquadFilterParameter, MaxBiquadFilters> biquads{}; 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 /// Number of active wavebuffers
u32 wave_buffer_count{}; u32 wave_buffer_count{};
/// Current playing wavebuffer index /// Current playing wavebuffer index

View File

@@ -1,4 +1,5 @@
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project // SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
@@ -44,8 +45,7 @@ struct Lifo {
buffer_count++; buffer_count++;
} }
buffer_tail = GetNextEntryIndex(); buffer_tail = GetNextEntryIndex();
const auto& previous_entry = ReadPreviousEntry(); entries[buffer_tail].sampling_number = new_state.sampling_number << 1;
entries[buffer_tail].sampling_number = previous_entry.sampling_number + 1;
entries[buffer_tail].state = new_state; entries[buffer_tail].state = new_state;
} }
}; };