mirror of
https://git.citron-emu.org/citron/emulator
synced 2025-12-19 10:43:33 +00:00
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:
@@ -30,15 +30,27 @@ add_library(audio_core STATIC
|
||||
audio_out_manager.h
|
||||
audio_manager.cpp
|
||||
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/common.h
|
||||
common/feature_support.h
|
||||
common/fft.cpp
|
||||
common/fft.h
|
||||
common/loudness_calculator.cpp
|
||||
common/loudness_calculator.h
|
||||
common/wave_buffer.h
|
||||
common/workbuffer_allocator.h
|
||||
device/audio_buffer.h
|
||||
device/audio_buffer_list.h
|
||||
device/audio_buffers.h
|
||||
device/device_session.cpp
|
||||
device/device_session.h
|
||||
device/shared_ring_buffer.cpp
|
||||
device/shared_ring_buffer.h
|
||||
in/audio_in.cpp
|
||||
in/audio_in.h
|
||||
in/audio_in_system.cpp
|
||||
@@ -85,6 +97,8 @@ add_library(audio_core STATIC
|
||||
renderer/command/effect/i3dl2_reverb.h
|
||||
renderer/command/effect/light_limiter.cpp
|
||||
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.h
|
||||
renderer/command/effect/reverb.cpp
|
||||
@@ -149,8 +163,13 @@ add_library(audio_core STATIC
|
||||
renderer/effect/i3dl2.h
|
||||
renderer/effect/light_limiter.cpp
|
||||
renderer/effect/light_limiter.h
|
||||
renderer/effect/limiter.cpp
|
||||
renderer/effect/limiter.h
|
||||
renderer/effect/reverb.h
|
||||
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.h
|
||||
renderer/mix/mix_info.cpp
|
||||
@@ -218,6 +237,7 @@ if (MSVC)
|
||||
/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
|
||||
/we4800 # Implicit conversion from 'type' to bool. Possible information loss
|
||||
/wd2375 # Disable C2375: '__builtin_assume_aligned': redefinition (MSVC 14.44+ issue)
|
||||
)
|
||||
else()
|
||||
target_compile_options(audio_core PRIVATE
|
||||
|
||||
78
src/audio_core/audio_snoop_manager.cpp
Normal file
78
src/audio_core/audio_snoop_manager.cpp
Normal 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
|
||||
100
src/audio_core/audio_snoop_manager.h
Normal file
100
src/audio_core/audio_snoop_manager.h
Normal 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
|
||||
128
src/audio_core/audio_system_manager.cpp
Normal file
128
src/audio_core/audio_system_manager.cpp
Normal 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
|
||||
137
src/audio_core/audio_system_manager.h
Normal file
137
src/audio_core/audio_system_manager.h
Normal 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
|
||||
165
src/audio_core/common/audio_helpers.h
Normal file
165
src/audio_core/common/audio_helpers.h
Normal 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
|
||||
@@ -1,4 +1,5 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
@@ -13,7 +14,7 @@
|
||||
#include "common/polyfill_ranges.h"
|
||||
|
||||
namespace AudioCore {
|
||||
constexpr u32 CurrentRevision = 13;
|
||||
constexpr u32 CurrentRevision = 15;
|
||||
|
||||
enum class SupportTags {
|
||||
CommandProcessingTimeEstimatorVersion4,
|
||||
@@ -46,6 +47,8 @@ enum class SupportTags {
|
||||
I3dl2ReverbChannelMappingChange,
|
||||
CompressorStatistics,
|
||||
SplitterPrevVolumeReset,
|
||||
SplitterDestinationV2b,
|
||||
VoiceInParameterV2,
|
||||
|
||||
// Not a real tag, just here to get the count.
|
||||
Size
|
||||
@@ -91,6 +94,9 @@ constexpr bool CheckFeatureSupported(SupportTags tag, u32 user_revision) {
|
||||
{SupportTags::I3dl2ReverbChannelMappingChange, 11},
|
||||
{SupportTags::CompressorStatistics, 13},
|
||||
{SupportTags::SplitterPrevVolumeReset, 13},
|
||||
{SupportTags::DeviceApiVersion2, 13},
|
||||
{SupportTags::SplitterDestinationV2b, 15},
|
||||
{SupportTags::VoiceInParameterV2, 15},
|
||||
}};
|
||||
|
||||
const auto& feature =
|
||||
|
||||
167
src/audio_core/common/fft.cpp
Normal file
167
src/audio_core/common/fft.cpp
Normal 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
|
||||
99
src/audio_core/common/fft.h
Normal file
99
src/audio_core/common/fft.h
Normal 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
|
||||
208
src/audio_core/common/loudness_calculator.cpp
Normal file
208
src/audio_core/common/loudness_calculator.cpp
Normal 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
|
||||
131
src/audio_core/common/loudness_calculator.h
Normal file
131
src/audio_core/common/loudness_calculator.h
Normal 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
|
||||
93
src/audio_core/device/audio_buffer_list.h
Normal file
93
src/audio_core/device/audio_buffer_list.h
Normal 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
|
||||
169
src/audio_core/device/shared_ring_buffer.cpp
Normal file
169
src/audio_core/device/shared_ring_buffer.cpp
Normal 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
|
||||
167
src/audio_core/device/shared_ring_buffer.h
Normal file
167
src/audio_core/device/shared_ring_buffer.h
Normal 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
|
||||
@@ -206,6 +206,16 @@ void System::SetVolume(const f32 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 {
|
||||
return buffers.ContainsBuffer(tag);
|
||||
}
|
||||
|
||||
@@ -213,6 +213,20 @@ public:
|
||||
*/
|
||||
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?
|
||||
*
|
||||
@@ -269,6 +283,8 @@ private:
|
||||
std::string name{};
|
||||
/// Volume of this system
|
||||
f32 volume{1.0f};
|
||||
/// Device gain of this system
|
||||
f32 device_gain{1.0f};
|
||||
/// Is this system's device USB?
|
||||
bool is_uac{false};
|
||||
};
|
||||
|
||||
@@ -198,4 +198,12 @@ bool BehaviorInfo::IsSplitterPrevVolumeResetSupported() const {
|
||||
return CheckFeatureSupported(SupportTags::SplitterPrevVolumeReset, user_revision);
|
||||
}
|
||||
|
||||
bool BehaviorInfo::IsSplitterDestinationV2bSupported() const {
|
||||
return CheckFeatureSupported(SupportTags::SplitterDestinationV2b, user_revision);
|
||||
}
|
||||
|
||||
bool BehaviorInfo::IsVoiceInParameterV2Supported() const {
|
||||
return CheckFeatureSupported(SupportTags::VoiceInParameterV2, user_revision);
|
||||
}
|
||||
|
||||
} // namespace AudioCore::Renderer
|
||||
|
||||
@@ -378,6 +378,22 @@ public:
|
||||
*/
|
||||
bool IsSplitterPrevVolumeResetSupported() const;
|
||||
|
||||
/**
|
||||
* Check if splitter destination v2b parameter format is supported (revision 15+).
|
||||
* This uses the extended parameter format with biquad filter fields.
|
||||
*
|
||||
* @return True if supported, otherwise false.
|
||||
*/
|
||||
bool IsSplitterDestinationV2bSupported() const;
|
||||
|
||||
/**
|
||||
* Check if voice input parameter v2 format is supported (revision 15+).
|
||||
* This uses the extended parameter format with float biquad filters.
|
||||
*
|
||||
* @return True if supported, otherwise false.
|
||||
*/
|
||||
bool IsVoiceInParameterV2Supported() const;
|
||||
|
||||
/// Host version
|
||||
u32 process_revision;
|
||||
/// User version
|
||||
|
||||
@@ -61,8 +61,6 @@ Result InfoUpdater::UpdateVoices(VoiceContext& voice_context,
|
||||
const PoolMapper pool_mapper(process_handle, memory_pools, memory_pool_count,
|
||||
behaviour.IsMemoryForceMappingEnabled());
|
||||
const auto voice_count{voice_context.GetCount()};
|
||||
std::span<const VoiceInfo::InParameter> in_params{
|
||||
reinterpret_cast<const VoiceInfo::InParameter*>(input), voice_count};
|
||||
std::span<VoiceInfo::OutStatus> out_params{reinterpret_cast<VoiceInfo::OutStatus*>(output),
|
||||
voice_count};
|
||||
|
||||
@@ -73,8 +71,97 @@ Result InfoUpdater::UpdateVoices(VoiceContext& voice_context,
|
||||
|
||||
u32 new_voice_count{0};
|
||||
|
||||
// Two input formats exist: legacy (0x170) and v2 with float biquad (0x188).
|
||||
const bool use_v2 = behaviour.IsVoiceInParameterV2Supported();
|
||||
const u32 in_stride = use_v2 ? 0x188u : static_cast<u32>(sizeof(VoiceInfo::InParameter));
|
||||
|
||||
for (u32 i = 0; i < voice_count; i++) {
|
||||
const auto& in_param{in_params[i]};
|
||||
VoiceInfo::InParameter local_in{};
|
||||
// Store original float biquad coefficients for REV15+
|
||||
std::array<VoiceInfo::BiquadFilterParameter2, MaxBiquadFilters> float_biquads{};
|
||||
|
||||
if (!use_v2) {
|
||||
const auto* in_param_ptr = reinterpret_cast<const VoiceInfo::InParameter*>(input + i * sizeof(VoiceInfo::InParameter));
|
||||
local_in = *in_param_ptr;
|
||||
} else {
|
||||
struct VoiceInParameterV2 {
|
||||
u32 id;
|
||||
u32 node_id;
|
||||
bool is_new;
|
||||
bool in_use;
|
||||
PlayState play_state;
|
||||
SampleFormat sample_format;
|
||||
u32 sample_rate;
|
||||
u32 priority;
|
||||
u32 sort_order;
|
||||
u32 channel_count;
|
||||
f32 pitch;
|
||||
f32 volume;
|
||||
// Two BiquadFilterParameter2 (0x18 each) -> ignored/converted
|
||||
struct BiquadV2 { bool enable; u8 r1; u8 r2; u8 r3; std::array<f32,3> b; std::array<f32,2> a; } biquads[2];
|
||||
u32 wave_buffer_count;
|
||||
u32 wave_buffer_index;
|
||||
u32 reserved1;
|
||||
u64 src_data_address;
|
||||
u64 src_data_size;
|
||||
s32 mix_id;
|
||||
u32 splitter_id;
|
||||
std::array<VoiceInfo::WaveBufferInternal, MaxWaveBuffers> wavebuffers;
|
||||
std::array<u32, MaxChannels> channel_resource_ids;
|
||||
bool clear_voice_drop;
|
||||
u8 flush_wave_buffer_count;
|
||||
u16 reserved2;
|
||||
VoiceInfo::Flags flags;
|
||||
SrcQuality src_quality;
|
||||
u32 external_ctx;
|
||||
u32 external_ctx_size;
|
||||
u32 reserved3[2];
|
||||
};
|
||||
const auto* vin = reinterpret_cast<const VoiceInParameterV2*>(input + i * in_stride);
|
||||
local_in.id = vin->id;
|
||||
local_in.node_id = vin->node_id;
|
||||
local_in.is_new = vin->is_new;
|
||||
local_in.in_use = vin->in_use;
|
||||
local_in.play_state = vin->play_state;
|
||||
local_in.sample_format = vin->sample_format;
|
||||
local_in.sample_rate = vin->sample_rate;
|
||||
local_in.priority = static_cast<s32>(vin->priority);
|
||||
local_in.sort_order = static_cast<s32>(vin->sort_order);
|
||||
local_in.channel_count = vin->channel_count;
|
||||
local_in.pitch = vin->pitch;
|
||||
local_in.volume = vin->volume;
|
||||
|
||||
// For REV15+, we keep float coefficients separate and only convert for compatibility
|
||||
for (size_t filter_idx = 0; filter_idx < MaxBiquadFilters; filter_idx++) {
|
||||
const auto& src = vin->biquads[filter_idx];
|
||||
auto& dst = local_in.biquads[filter_idx];
|
||||
dst.enabled = src.enable;
|
||||
// Convert float coefficients to fixed-point Q2.14 for legacy path
|
||||
dst.b[0] = static_cast<s16>(std::clamp(src.b[0] * 16384.0f, -32768.0f, 32767.0f));
|
||||
dst.b[1] = static_cast<s16>(std::clamp(src.b[1] * 16384.0f, -32768.0f, 32767.0f));
|
||||
dst.b[2] = static_cast<s16>(std::clamp(src.b[2] * 16384.0f, -32768.0f, 32767.0f));
|
||||
dst.a[0] = static_cast<s16>(std::clamp(src.a[0] * 16384.0f, -32768.0f, 32767.0f));
|
||||
dst.a[1] = static_cast<s16>(std::clamp(src.a[1] * 16384.0f, -32768.0f, 32767.0f));
|
||||
|
||||
// Also store the native float version
|
||||
float_biquads[filter_idx].enabled = src.enable;
|
||||
float_biquads[filter_idx].numerator = src.b;
|
||||
float_biquads[filter_idx].denominator = src.a;
|
||||
}
|
||||
local_in.wave_buffer_count = vin->wave_buffer_count;
|
||||
local_in.wave_buffer_index = static_cast<u16>(vin->wave_buffer_index);
|
||||
local_in.src_data_address = static_cast<CpuAddr>(vin->src_data_address);
|
||||
local_in.src_data_size = vin->src_data_size;
|
||||
local_in.mix_id = static_cast<u32>(vin->mix_id);
|
||||
local_in.splitter_id = vin->splitter_id;
|
||||
local_in.wave_buffer_internal = vin->wavebuffers;
|
||||
local_in.channel_resource_ids = vin->channel_resource_ids;
|
||||
local_in.clear_voice_drop = vin->clear_voice_drop;
|
||||
local_in.flush_buffer_count = vin->flush_wave_buffer_count;
|
||||
local_in.flags = vin->flags;
|
||||
local_in.src_quality = vin->src_quality;
|
||||
}
|
||||
const auto& in_param = local_in;
|
||||
std::array<VoiceState*, MaxChannels> voice_states{};
|
||||
|
||||
if (!in_param.in_use) {
|
||||
@@ -98,6 +185,14 @@ Result InfoUpdater::UpdateVoices(VoiceContext& voice_context,
|
||||
BehaviorInfo::ErrorInfo update_error{};
|
||||
voice_info.UpdateParameters(update_error, in_param, pool_mapper, behaviour);
|
||||
|
||||
// For REV15+, store the native float biquad coefficients
|
||||
if (use_v2) {
|
||||
voice_info.use_float_biquads = true;
|
||||
voice_info.biquads_float = float_biquads;
|
||||
} else {
|
||||
voice_info.use_float_biquads = false;
|
||||
}
|
||||
|
||||
if (!update_error.error_code.IsSuccess()) {
|
||||
behaviour.AppendError(update_error);
|
||||
}
|
||||
@@ -118,7 +213,7 @@ Result InfoUpdater::UpdateVoices(VoiceContext& voice_context,
|
||||
new_voice_count += in_param.channel_count;
|
||||
}
|
||||
|
||||
auto consumed_input_size{voice_count * static_cast<u32>(sizeof(VoiceInfo::InParameter))};
|
||||
auto consumed_input_size{voice_count * in_stride};
|
||||
auto consumed_output_size{voice_count * static_cast<u32>(sizeof(VoiceInfo::OutStatus))};
|
||||
if (consumed_input_size != in_header->voices_size) {
|
||||
LOG_ERROR(Service_Audio, "Consumed an incorrect voices size, header size={}, consumed={}",
|
||||
@@ -254,18 +349,29 @@ Result InfoUpdater::UpdateMixes(MixContext& mix_context, const u32 mix_buffer_co
|
||||
EffectContext& effect_context, SplitterContext& splitter_context) {
|
||||
s32 mix_count{0};
|
||||
u32 consumed_input_size{0};
|
||||
u32 input_mix_size{0};
|
||||
|
||||
if (behaviour.IsMixInParameterDirtyOnlyUpdateSupported()) {
|
||||
auto in_dirty_params{reinterpret_cast<const MixInfo::InDirtyParameter*>(input)};
|
||||
mix_count = in_dirty_params->count;
|
||||
|
||||
// Validate against expected header size to ensure structure is correct
|
||||
if (mix_count < 0 || mix_count > 0x100) {
|
||||
LOG_ERROR(Service_Audio,
|
||||
"Invalid mix count from dirty parameter: count={}, magic=0x{:X}, expected_size={}",
|
||||
mix_count, in_dirty_params->magic, in_header->mix_size);
|
||||
return Service::Audio::ResultInvalidUpdateInfo;
|
||||
}
|
||||
|
||||
consumed_input_size += static_cast<u32>(sizeof(MixInfo::InDirtyParameter));
|
||||
input += sizeof(MixInfo::InDirtyParameter);
|
||||
consumed_input_size = static_cast<u32>(sizeof(MixInfo::InDirtyParameter) +
|
||||
mix_count * sizeof(MixInfo::InParameter));
|
||||
} else {
|
||||
mix_count = mix_context.GetCount();
|
||||
consumed_input_size = static_cast<u32>(mix_count * sizeof(MixInfo::InParameter));
|
||||
}
|
||||
|
||||
input_mix_size = static_cast<u32>(mix_count * sizeof(MixInfo::InParameter));
|
||||
consumed_input_size += input_mix_size;
|
||||
|
||||
if (mix_buffer_count == 0) {
|
||||
return Service::Audio::ResultInvalidUpdateInfo;
|
||||
}
|
||||
@@ -330,7 +436,7 @@ Result InfoUpdater::UpdateMixes(MixContext& mix_context, const u32 mix_buffer_co
|
||||
return Service::Audio::ResultInvalidUpdateInfo;
|
||||
}
|
||||
|
||||
input += mix_count * sizeof(MixInfo::InParameter);
|
||||
input += input_mix_size;
|
||||
|
||||
return ResultSuccess;
|
||||
}
|
||||
|
||||
@@ -234,6 +234,14 @@ void CommandBuffer::GenerateBiquadFilterCommand(const s32 node_id, VoiceInfo& vo
|
||||
|
||||
cmd.biquad = voice_info.biquads[biquad_index];
|
||||
|
||||
// REV15+: Use native float coefficients if available
|
||||
if (voice_info.use_float_biquads) {
|
||||
cmd.biquad_float = voice_info.biquads_float[biquad_index];
|
||||
cmd.use_float_coefficients = true;
|
||||
} else {
|
||||
cmd.use_float_coefficients = false;
|
||||
}
|
||||
|
||||
cmd.state = memory_pool->Translate(CpuAddr(voice_state.biquad_states[biquad_index].data()),
|
||||
MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState));
|
||||
|
||||
@@ -260,6 +268,9 @@ void CommandBuffer::GenerateBiquadFilterCommand(const s32 node_id, EffectInfoBas
|
||||
cmd.biquad.b = parameter.b;
|
||||
cmd.biquad.a = parameter.a;
|
||||
|
||||
// Effects use legacy fixed-point format
|
||||
cmd.use_float_coefficients = false;
|
||||
|
||||
cmd.state = memory_pool->Translate(CpuAddr(state),
|
||||
MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState));
|
||||
|
||||
@@ -655,6 +666,14 @@ void CommandBuffer::GenerateMultitapBiquadFilterCommand(const s32 node_id, Voice
|
||||
cmd.output = buffer_count + channel;
|
||||
cmd.biquads = voice_info.biquads;
|
||||
|
||||
// REV15+: Use native float coefficients if available
|
||||
if (voice_info.use_float_biquads) {
|
||||
cmd.biquads_float = voice_info.biquads_float;
|
||||
cmd.use_float_coefficients = true;
|
||||
} else {
|
||||
cmd.use_float_coefficients = false;
|
||||
}
|
||||
|
||||
cmd.states[0] =
|
||||
memory_pool->Translate(CpuAddr(voice_state.biquad_states[0].data()),
|
||||
MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState));
|
||||
|
||||
@@ -48,6 +48,39 @@ void ApplyBiquadFilterFloat(std::span<s32> output, std::span<const s32> input,
|
||||
state.s3 = Common::BitCast<s64>(s[3]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Biquad filter float implementation with native float coefficients (SDK REV15+).
|
||||
*/
|
||||
void ApplyBiquadFilterFloat2(std::span<s32> output, std::span<const s32> input,
|
||||
std::array<f32, 3>& b, std::array<f32, 2>& a,
|
||||
VoiceState::BiquadFilterState& state, const u32 sample_count) {
|
||||
constexpr f64 min{std::numeric_limits<s32>::min()};
|
||||
constexpr f64 max{std::numeric_limits<s32>::max()};
|
||||
|
||||
std::array<f64, 3> b_double{static_cast<f64>(b[0]), static_cast<f64>(b[1]), static_cast<f64>(b[2])};
|
||||
std::array<f64, 2> a_double{static_cast<f64>(a[0]), static_cast<f64>(a[1])};
|
||||
std::array<f64, 4> s{Common::BitCast<f64>(state.s0), Common::BitCast<f64>(state.s1),
|
||||
Common::BitCast<f64>(state.s2), Common::BitCast<f64>(state.s3)};
|
||||
|
||||
for (u32 i = 0; i < sample_count; i++) {
|
||||
f64 in_sample{static_cast<f64>(input[i])};
|
||||
auto sample{in_sample * b_double[0] + s[0] * b_double[1] + s[1] * b_double[2] +
|
||||
s[2] * a_double[0] + s[3] * a_double[1]};
|
||||
|
||||
output[i] = static_cast<s32>(std::clamp(sample, min, max));
|
||||
|
||||
s[1] = s[0];
|
||||
s[0] = in_sample;
|
||||
s[3] = s[2];
|
||||
s[2] = sample;
|
||||
}
|
||||
|
||||
state.s0 = Common::BitCast<s64>(s[0]);
|
||||
state.s1 = Common::BitCast<s64>(s[1]);
|
||||
state.s2 = Common::BitCast<s64>(s[2]);
|
||||
state.s3 = Common::BitCast<s64>(s[3]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Biquad filter s32 implementation.
|
||||
*
|
||||
@@ -95,8 +128,14 @@ void BiquadFilterCommand::Process(const AudioRenderer::CommandListProcessor& pro
|
||||
processor.mix_buffers.subspan(output * processor.sample_count, processor.sample_count)};
|
||||
|
||||
if (use_float_processing) {
|
||||
// REV15+: Use native float coefficients if available
|
||||
if (use_float_coefficients) {
|
||||
ApplyBiquadFilterFloat2(output_buffer, input_buffer, biquad_float.numerator,
|
||||
biquad_float.denominator, *state_, processor.sample_count);
|
||||
} else {
|
||||
ApplyBiquadFilterFloat(output_buffer, input_buffer, biquad.b, biquad.a, *state_,
|
||||
processor.sample_count);
|
||||
}
|
||||
} else {
|
||||
ApplyBiquadFilterInt(output_buffer, input_buffer, biquad.b, biquad.a, *state_,
|
||||
processor.sample_count);
|
||||
|
||||
@@ -48,14 +48,18 @@ struct BiquadFilterCommand : ICommand {
|
||||
s16 input;
|
||||
/// Output mix buffer index
|
||||
s16 output;
|
||||
/// Input parameters for biquad
|
||||
/// Input parameters for biquad (legacy fixed-point)
|
||||
VoiceInfo::BiquadFilterParameter biquad;
|
||||
/// Input parameters for biquad (REV15+ native float)
|
||||
VoiceInfo::BiquadFilterParameter2 biquad_float;
|
||||
/// Biquad state, updated each call
|
||||
CpuAddr state;
|
||||
/// If true, reset the state
|
||||
bool needs_init;
|
||||
/// If true, use float processing rather than int
|
||||
bool use_float_processing;
|
||||
/// If true, use native float coefficients (REV15+)
|
||||
bool use_float_coefficients;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -72,4 +76,18 @@ void ApplyBiquadFilterFloat(std::span<s32> output, std::span<const s32> input,
|
||||
std::array<s16, 3>& b, std::array<s16, 2>& a,
|
||||
VoiceState::BiquadFilterState& state, const u32 sample_count);
|
||||
|
||||
/**
|
||||
* Biquad filter float implementation with native float coefficients (SDK REV15+).
|
||||
*
|
||||
* @param output - Output container for filtered samples.
|
||||
* @param input - Input container for samples to be filtered.
|
||||
* @param b - Feedforward coefficients (float).
|
||||
* @param a - Feedback coefficients (float).
|
||||
* @param state - State to track previous samples.
|
||||
* @param sample_count - Number of samples to process.
|
||||
*/
|
||||
void ApplyBiquadFilterFloat2(std::span<s32> output, std::span<const s32> input,
|
||||
std::array<f32, 3>& b, std::array<f32, 2>& a,
|
||||
VoiceState::BiquadFilterState& state, const u32 sample_count);
|
||||
|
||||
} // namespace AudioCore::Renderer
|
||||
|
||||
90
src/audio_core/renderer/command/effect/limiter.cpp
Normal file
90
src/audio_core/renderer/command/effect/limiter.cpp
Normal 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
|
||||
60
src/audio_core/renderer/command/effect/limiter.h
Normal file
60
src/audio_core/renderer/command/effect/limiter.h
Normal 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
|
||||
@@ -33,9 +33,15 @@ void MultiTapBiquadFilterCommand::Process(const AudioRenderer::CommandListProces
|
||||
*state = {};
|
||||
}
|
||||
|
||||
// REV15+: Use native float coefficients if available
|
||||
if (use_float_coefficients) {
|
||||
ApplyBiquadFilterFloat2(output_buffer, input_buffer, biquads_float[i].numerator,
|
||||
biquads_float[i].denominator, *state, processor.sample_count);
|
||||
} else {
|
||||
ApplyBiquadFilterFloat(output_buffer, input_buffer, biquads[i].b, biquads[i].a, *state,
|
||||
processor.sample_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool MultiTapBiquadFilterCommand::Verify(const AudioRenderer::CommandListProcessor& processor) {
|
||||
|
||||
@@ -47,14 +47,18 @@ struct MultiTapBiquadFilterCommand : ICommand {
|
||||
s16 input;
|
||||
/// Output mix buffer index
|
||||
s16 output;
|
||||
/// Biquad parameters
|
||||
/// Biquad parameters (legacy fixed-point)
|
||||
std::array<VoiceInfo::BiquadFilterParameter, MaxBiquadFilters> biquads;
|
||||
/// Biquad parameters (REV15+ native float)
|
||||
std::array<VoiceInfo::BiquadFilterParameter2, MaxBiquadFilters> biquads_float;
|
||||
/// Biquad states, updated each call
|
||||
std::array<CpuAddr, MaxBiquadFilters> states;
|
||||
/// If each biquad needs initialisation
|
||||
std::array<bool, MaxBiquadFilters> needs_init;
|
||||
/// Number of active biquads
|
||||
u8 filter_tap_count;
|
||||
/// If true, use native float coefficients (REV15+)
|
||||
bool use_float_coefficients;
|
||||
};
|
||||
|
||||
} // namespace AudioCore::Renderer
|
||||
|
||||
67
src/audio_core/renderer/effect/limiter.cpp
Normal file
67
src/audio_core/renderer/effect/limiter.cpp
Normal 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
|
||||
101
src/audio_core/renderer/effect/limiter.h
Normal file
101
src/audio_core/renderer/effect/limiter.h
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -40,6 +40,7 @@ void SplitterContext::Setup(std::span<SplitterInfo> splitter_infos_, const u32 s
|
||||
destinations_count = destination_count_;
|
||||
splitter_bug_fixed = splitter_bug_fixed_;
|
||||
splitter_prev_volume_reset_supported = behavior.IsSplitterPrevVolumeResetSupported();
|
||||
splitter_float_coeff_supported = behavior.IsSplitterDestinationV2bSupported();
|
||||
}
|
||||
|
||||
bool SplitterContext::UsingSplitter() const {
|
||||
@@ -136,25 +137,57 @@ u32 SplitterContext::UpdateInfo(const u8* input, u32 offset, const u32 splitter_
|
||||
|
||||
u32 SplitterContext::UpdateData(const u8* input, u32 offset, const u32 count) {
|
||||
for (u32 i = 0; i < count; i++) {
|
||||
auto data_header{
|
||||
reinterpret_cast<const SplitterDestinationData::InParameter*>(input + offset)};
|
||||
// Version selection based on float coeff/biquad v2b support.
|
||||
if (!splitter_float_coeff_supported) {
|
||||
const auto* data_header =
|
||||
reinterpret_cast<const SplitterDestinationData::InParameter*>(input + offset);
|
||||
|
||||
if (data_header->magic != GetSplitterSendDataMagic()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (data_header->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);
|
||||
} 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));
|
||||
}
|
||||
}
|
||||
|
||||
return offset;
|
||||
|
||||
@@ -186,6 +186,8 @@ private:
|
||||
bool splitter_bug_fixed{};
|
||||
/// Is explicit previous mix volume reset supported?
|
||||
bool splitter_prev_volume_reset_supported{};
|
||||
/// Is float coefficient/biquad filter v2b parameter supported?
|
||||
bool splitter_float_coeff_supported{};
|
||||
};
|
||||
|
||||
} // namespace Renderer
|
||||
|
||||
@@ -87,4 +87,12 @@ void SplitterDestinationData::SetNext(SplitterDestinationData* next_) {
|
||||
next = next_;
|
||||
}
|
||||
|
||||
std::span<SplitterDestinationData::BiquadFilterParameter2> SplitterDestinationData::GetBiquadFilters() {
|
||||
return biquad_filters;
|
||||
}
|
||||
|
||||
std::span<const SplitterDestinationData::BiquadFilterParameter2> SplitterDestinationData::GetBiquadFilters() const {
|
||||
return biquad_filters;
|
||||
}
|
||||
|
||||
} // namespace AudioCore::Renderer
|
||||
|
||||
@@ -10,12 +10,31 @@
|
||||
#include "common/common_types.h"
|
||||
|
||||
namespace AudioCore::Renderer {
|
||||
|
||||
// Forward declaration
|
||||
class VoiceInfo;
|
||||
|
||||
/**
|
||||
* Represents a mixing node, can be connected to a previous and next destination forming a chain
|
||||
* that a certain mix buffer will pass through to output.
|
||||
*/
|
||||
class SplitterDestinationData {
|
||||
public:
|
||||
/**
|
||||
* Biquad filter parameter with float coefficients (SDK REV15+).
|
||||
* Defined here to avoid circular dependency with VoiceInfo.
|
||||
*/
|
||||
struct BiquadFilterParameter2 {
|
||||
/* 0x00 */ bool enabled;
|
||||
/* 0x01 */ u8 reserved1;
|
||||
/* 0x02 */ u8 reserved2;
|
||||
/* 0x03 */ u8 reserved3;
|
||||
/* 0x04 */ std::array<f32, 3> numerator; // b0, b1, b2
|
||||
/* 0x10 */ std::array<f32, 2> denominator; // a1, a2 (a0 = 1)
|
||||
};
|
||||
static_assert(sizeof(BiquadFilterParameter2) == 0x18,
|
||||
"BiquadFilterParameter2 has the wrong size!");
|
||||
|
||||
struct InParameter {
|
||||
/* 0x00 */ u32 magic; // 'SNDD'
|
||||
/* 0x04 */ s32 id;
|
||||
@@ -27,6 +46,19 @@ public:
|
||||
static_assert(sizeof(InParameter) == 0x70,
|
||||
"SplitterDestinationData::InParameter has the wrong size!");
|
||||
|
||||
struct InParameterVersion2b {
|
||||
/* 0x00 */ u32 magic; // 'SNDD'
|
||||
/* 0x04 */ s32 id;
|
||||
/* 0x08 */ std::array<f32, MaxMixBuffers> mix_volumes;
|
||||
/* 0x68 */ u32 mix_id;
|
||||
/* 0x6C */ std::array<SplitterDestinationData::BiquadFilterParameter2, MaxBiquadFilters> biquad_filters;
|
||||
/* 0x9C */ bool in_use;
|
||||
/* 0x9D */ bool reset_prev_volume;
|
||||
/* 0x9E */ u8 reserved[10];
|
||||
};
|
||||
static_assert(sizeof(InParameterVersion2b) == 0xA8,
|
||||
"SplitterDestinationData::InParameterVersion2b has the wrong size!");
|
||||
|
||||
SplitterDestinationData(s32 id);
|
||||
|
||||
/**
|
||||
@@ -116,6 +148,20 @@ public:
|
||||
*/
|
||||
void SetNext(SplitterDestinationData* next);
|
||||
|
||||
/**
|
||||
* Get biquad filter parameters for this destination (REV15+).
|
||||
*
|
||||
* @return Span of biquad filter parameters.
|
||||
*/
|
||||
std::span<BiquadFilterParameter2> GetBiquadFilters();
|
||||
|
||||
/**
|
||||
* Get const biquad filter parameters for this destination (REV15+).
|
||||
*
|
||||
* @return Const span of biquad filter parameters.
|
||||
*/
|
||||
std::span<const BiquadFilterParameter2> GetBiquadFilters() const;
|
||||
|
||||
private:
|
||||
/// Id of this destination
|
||||
const s32 id;
|
||||
@@ -125,6 +171,8 @@ private:
|
||||
std::array<f32, MaxMixBuffers> mix_volumes{0.0f};
|
||||
/// Previous mix volumes
|
||||
std::array<f32, MaxMixBuffers> prev_mix_volumes{0.0f};
|
||||
/// Biquad filter parameters (REV15+)
|
||||
std::array<BiquadFilterParameter2, MaxBiquadFilters> biquad_filters{};
|
||||
/// Next destination in the mix chain
|
||||
SplitterDestinationData* next{};
|
||||
/// Is this destination in use?
|
||||
|
||||
@@ -135,6 +135,17 @@ public:
|
||||
static_assert(sizeof(BiquadFilterParameter) == 0xC,
|
||||
"VoiceInfo::BiquadFilterParameter has the wrong size!");
|
||||
|
||||
struct BiquadFilterParameter2 {
|
||||
/* 0x00 */ bool enabled;
|
||||
/* 0x01 */ u8 reserved1;
|
||||
/* 0x02 */ u8 reserved2;
|
||||
/* 0x03 */ u8 reserved3;
|
||||
/* 0x04 */ std::array<f32, 3> numerator; // b0, b1, b2
|
||||
/* 0x10 */ std::array<f32, 2> denominator; // a1, a2 (a0 = 1)
|
||||
};
|
||||
static_assert(sizeof(BiquadFilterParameter2) == 0x18,
|
||||
"VoiceInfo::BiquadFilterParameter2 has the wrong size!");
|
||||
|
||||
struct InParameter {
|
||||
/* 0x000 */ u32 id;
|
||||
/* 0x004 */ u32 node_id;
|
||||
@@ -168,6 +179,43 @@ public:
|
||||
};
|
||||
static_assert(sizeof(InParameter) == 0x170, "VoiceInfo::InParameter has the wrong size!");
|
||||
|
||||
struct InParameter2 {
|
||||
/* 0x000 */ u32 id;
|
||||
/* 0x004 */ u32 node_id;
|
||||
/* 0x008 */ bool is_new;
|
||||
/* 0x009 */ bool in_use;
|
||||
/* 0x00A */ PlayState play_state;
|
||||
/* 0x00B */ SampleFormat sample_format;
|
||||
/* 0x00C */ u32 sample_rate;
|
||||
/* 0x010 */ s32 priority;
|
||||
/* 0x014 */ s32 sort_order;
|
||||
/* 0x018 */ u32 channel_count;
|
||||
/* 0x01C */ f32 pitch;
|
||||
/* 0x020 */ f32 volume;
|
||||
/* 0x024 */ std::array<BiquadFilterParameter2, MaxBiquadFilters> biquads;
|
||||
/* 0x054 */ u32 wave_buffer_count;
|
||||
/* 0x058 */ u32 wave_buffer_index;
|
||||
/* 0x05C */ u32 reserved1;
|
||||
/* 0x060 */ CpuAddr src_data_address;
|
||||
/* 0x068 */ u64 src_data_size;
|
||||
/* 0x070 */ u32 mix_id;
|
||||
/* 0x074 */ u32 splitter_id;
|
||||
/* 0x078 */ std::array<WaveBufferInternal, MaxWaveBuffers> wave_buffer_internal;
|
||||
/* 0x158 */ std::array<s32, MaxChannels> channel_resource_ids;
|
||||
/* 0x170 */ bool clear_voice_drop;
|
||||
/* 0x171 */ u8 flush_buffer_count;
|
||||
/* 0x172 */ u16 reserved2;
|
||||
/* 0x174 */ Flags flags;
|
||||
/* 0x175 */ u8 reserved3;
|
||||
/* 0x176 */ SrcQuality src_quality;
|
||||
/* 0x177 */ u8 reserved4;
|
||||
/* 0x178 */ u32 external_context;
|
||||
/* 0x17C */ u32 external_context_size;
|
||||
/* 0x180 */ u32 reserved5;
|
||||
/* 0x184 */ u32 reserved6;
|
||||
};
|
||||
static_assert(sizeof(InParameter2) == 0x188, "VoiceInfo::InParameter2 has the wrong size!");
|
||||
|
||||
struct OutStatus {
|
||||
/* 0x00 */ u64 played_sample_count;
|
||||
/* 0x08 */ u32 wave_buffers_consumed;
|
||||
@@ -349,6 +397,10 @@ public:
|
||||
f32 prev_volume{};
|
||||
/// Biquad filters for generating filter commands on this voice
|
||||
std::array<BiquadFilterParameter, MaxBiquadFilters> biquads{};
|
||||
/// Float biquad filters for REV15+ (native float coefficients)
|
||||
std::array<BiquadFilterParameter2, MaxBiquadFilters> biquads_float{};
|
||||
/// Use float biquad coefficients (REV15+)
|
||||
bool use_float_biquads{};
|
||||
/// Number of active wavebuffers
|
||||
u32 wave_buffer_count{};
|
||||
/// Current playing wavebuffer index
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025 Citron Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
@@ -44,8 +45,7 @@ struct Lifo {
|
||||
buffer_count++;
|
||||
}
|
||||
buffer_tail = GetNextEntryIndex();
|
||||
const auto& previous_entry = ReadPreviousEntry();
|
||||
entries[buffer_tail].sampling_number = previous_entry.sampling_number + 1;
|
||||
entries[buffer_tail].sampling_number = new_state.sampling_number << 1;
|
||||
entries[buffer_tail].state = new_state;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user