2022-07-17 00:48:45 +02:00
|
|
|
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
|
|
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
|
|
|
|
#include <chrono>
|
|
|
|
#include <span>
|
|
|
|
|
2023-08-31 16:09:15 +02:00
|
|
|
#include "audio_core/adsp/apps/audio_renderer/audio_renderer.h"
|
|
|
|
#include "audio_core/adsp/apps/audio_renderer/command_buffer.h"
|
2022-07-17 00:48:45 +02:00
|
|
|
#include "audio_core/audio_core.h"
|
|
|
|
#include "audio_core/common/audio_renderer_parameter.h"
|
|
|
|
#include "audio_core/common/common.h"
|
|
|
|
#include "audio_core/common/feature_support.h"
|
|
|
|
#include "audio_core/common/workbuffer_allocator.h"
|
|
|
|
#include "audio_core/renderer/behavior/info_updater.h"
|
|
|
|
#include "audio_core/renderer/command/command_buffer.h"
|
|
|
|
#include "audio_core/renderer/command/command_generator.h"
|
|
|
|
#include "audio_core/renderer/command/command_list_header.h"
|
|
|
|
#include "audio_core/renderer/effect/effect_info_base.h"
|
|
|
|
#include "audio_core/renderer/effect/effect_result_state.h"
|
|
|
|
#include "audio_core/renderer/memory/memory_pool_info.h"
|
|
|
|
#include "audio_core/renderer/memory/pool_mapper.h"
|
|
|
|
#include "audio_core/renderer/mix/mix_info.h"
|
|
|
|
#include "audio_core/renderer/nodes/edge_matrix.h"
|
|
|
|
#include "audio_core/renderer/nodes/node_states.h"
|
|
|
|
#include "audio_core/renderer/sink/sink_info_base.h"
|
|
|
|
#include "audio_core/renderer/system.h"
|
|
|
|
#include "audio_core/renderer/upsampler/upsampler_info.h"
|
|
|
|
#include "audio_core/renderer/voice/voice_channel_resource.h"
|
|
|
|
#include "audio_core/renderer/voice/voice_info.h"
|
|
|
|
#include "audio_core/renderer/voice/voice_state.h"
|
|
|
|
#include "common/alignment.h"
|
|
|
|
#include "core/core.h"
|
|
|
|
#include "core/core_timing.h"
|
|
|
|
#include "core/hle/kernel/k_event.h"
|
|
|
|
#include "core/hle/kernel/k_transfer_memory.h"
|
|
|
|
#include "core/memory.h"
|
|
|
|
|
2023-08-31 16:09:15 +02:00
|
|
|
namespace AudioCore::Renderer {
|
2022-07-17 00:48:45 +02:00
|
|
|
|
|
|
|
u64 System::GetWorkBufferSize(const AudioRendererParameterInternal& params) {
|
|
|
|
BehaviorInfo behavior;
|
|
|
|
behavior.SetUserLibRevision(params.revision);
|
|
|
|
|
|
|
|
u64 size{0};
|
|
|
|
|
|
|
|
size += Common::AlignUp(params.mixes * sizeof(s32), 0x40);
|
|
|
|
size += params.sub_mixes * MaxEffects * sizeof(s32);
|
|
|
|
size += (params.sub_mixes + 1) * sizeof(MixInfo);
|
|
|
|
size += params.voices * (sizeof(VoiceInfo) + sizeof(VoiceChannelResource) + sizeof(VoiceState));
|
|
|
|
size += Common::AlignUp((params.sub_mixes + 1) * sizeof(MixInfo*), 0x10);
|
|
|
|
size += Common::AlignUp(params.voices * sizeof(VoiceInfo*), 0x10);
|
|
|
|
size += Common::AlignUp(((params.sinks + params.sub_mixes) * TargetSampleCount * sizeof(s32) +
|
|
|
|
params.sample_count * sizeof(s32)) *
|
|
|
|
(params.mixes + MaxChannels),
|
|
|
|
0x40);
|
|
|
|
|
|
|
|
if (behavior.IsSplitterSupported()) {
|
|
|
|
const auto node_size{NodeStates::GetWorkBufferSize(params.sub_mixes + 1)};
|
|
|
|
const auto edge_size{EdgeMatrix::GetWorkBufferSize(params.sub_mixes + 1)};
|
|
|
|
size += Common::AlignUp(node_size + edge_size, 0x10);
|
|
|
|
}
|
|
|
|
|
|
|
|
size += SplitterContext::CalcWorkBufferSize(behavior, params);
|
|
|
|
size += (params.effects + params.voices * MaxWaveBuffers) * sizeof(MemoryPoolInfo);
|
|
|
|
|
|
|
|
if (behavior.IsEffectInfoVersion2Supported()) {
|
|
|
|
size += params.effects * sizeof(EffectResultState);
|
|
|
|
}
|
|
|
|
size += 0x50;
|
|
|
|
|
|
|
|
size = Common::AlignUp(size, 0x40);
|
|
|
|
|
|
|
|
size += (params.sinks + params.sub_mixes) * sizeof(UpsamplerInfo);
|
|
|
|
size += params.effects * sizeof(EffectInfoBase);
|
|
|
|
size += Common::AlignUp(params.voices * sizeof(VoiceState), 0x40);
|
|
|
|
size += params.sinks * sizeof(SinkInfoBase);
|
|
|
|
|
|
|
|
if (behavior.IsEffectInfoVersion2Supported()) {
|
|
|
|
size += params.effects * sizeof(EffectResultState);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (params.perf_frames > 0) {
|
|
|
|
auto perf_size{PerformanceManager::GetRequiredBufferSizeForPerformanceMetricsPerFrame(
|
|
|
|
behavior, params)};
|
|
|
|
size += Common::AlignUp(perf_size * (params.perf_frames + 1) + 0xC0, 0x100);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (behavior.IsVariadicCommandBufferSizeSupported()) {
|
|
|
|
size += CommandGenerator::CalculateCommandBufferSize(behavior, params) + (0x40 - 1) * 2;
|
|
|
|
} else {
|
|
|
|
size += 0x18000 + (0x40 - 1) * 2;
|
|
|
|
}
|
|
|
|
|
|
|
|
size = Common::AlignUp(size, 0x1000);
|
|
|
|
return size;
|
|
|
|
}
|
|
|
|
|
|
|
|
System::System(Core::System& core_, Kernel::KEvent* adsp_rendered_event_)
|
2023-08-31 16:09:15 +02:00
|
|
|
: core{core_}, audio_renderer{core.AudioCore().ADSP().AudioRenderer()},
|
|
|
|
adsp_rendered_event{adsp_rendered_event_} {}
|
2022-07-17 00:48:45 +02:00
|
|
|
|
|
|
|
Result System::Initialize(const AudioRendererParameterInternal& params,
|
2022-10-19 06:38:12 +02:00
|
|
|
Kernel::KTransferMemory* transfer_memory, u64 transfer_memory_size,
|
|
|
|
u32 process_handle_, u64 applet_resource_user_id_, s32 session_id_) {
|
2022-07-17 00:48:45 +02:00
|
|
|
if (!CheckValidRevision(params.revision)) {
|
2023-03-07 01:04:12 +01:00
|
|
|
return Service::Audio::ResultInvalidRevision;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (GetWorkBufferSize(params) > transfer_memory_size) {
|
2023-03-07 01:04:12 +01:00
|
|
|
return Service::Audio::ResultInsufficientBuffer;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (process_handle_ == 0) {
|
2023-03-07 01:04:12 +01:00
|
|
|
return Service::Audio::ResultInvalidHandle;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
behavior.SetUserLibRevision(params.revision);
|
|
|
|
|
|
|
|
process_handle = process_handle_;
|
|
|
|
applet_resource_user_id = applet_resource_user_id_;
|
|
|
|
session_id = session_id_;
|
|
|
|
|
|
|
|
sample_rate = params.sample_rate;
|
|
|
|
sample_count = params.sample_count;
|
|
|
|
mix_buffer_count = static_cast<s16>(params.mixes);
|
|
|
|
voice_channels = MaxChannels;
|
|
|
|
upsampler_count = params.sinks + params.sub_mixes;
|
|
|
|
memory_pool_count = params.effects + params.voices * MaxWaveBuffers;
|
|
|
|
render_device = params.rendering_device;
|
|
|
|
execution_mode = params.execution_mode;
|
|
|
|
|
2023-03-24 00:58:48 +01:00
|
|
|
core.ApplicationMemory().ZeroBlock(transfer_memory->GetSourceAddress(), transfer_memory_size);
|
2022-07-17 00:48:45 +02:00
|
|
|
|
|
|
|
// Note: We're not actually using the transfer memory because it's a pain to code for.
|
|
|
|
// Allocate the memory normally instead and hope the game doesn't try to read anything back
|
|
|
|
workbuffer = std::make_unique<u8[]>(transfer_memory_size);
|
|
|
|
workbuffer_size = transfer_memory_size;
|
|
|
|
|
|
|
|
PoolMapper pool_mapper(process_handle, false);
|
|
|
|
pool_mapper.InitializeSystemPool(memory_pool_info, workbuffer.get(), workbuffer_size);
|
|
|
|
|
|
|
|
WorkbufferAllocator allocator({workbuffer.get(), workbuffer_size}, workbuffer_size);
|
|
|
|
|
|
|
|
samples_workbuffer =
|
|
|
|
allocator.Allocate<s32>((voice_channels + mix_buffer_count) * sample_count, 0x10);
|
|
|
|
if (samples_workbuffer.empty()) {
|
2023-03-07 01:04:12 +01:00
|
|
|
return Service::Audio::ResultInsufficientBuffer;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
auto upsampler_workbuffer{allocator.Allocate<s32>(
|
|
|
|
(voice_channels + mix_buffer_count) * TargetSampleCount * upsampler_count, 0x10)};
|
|
|
|
if (upsampler_workbuffer.empty()) {
|
2023-03-07 01:04:12 +01:00
|
|
|
return Service::Audio::ResultInsufficientBuffer;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
depop_buffer =
|
|
|
|
allocator.Allocate<s32>(Common::AlignUp(static_cast<u32>(mix_buffer_count), 0x40), 0x40);
|
|
|
|
if (depop_buffer.empty()) {
|
2023-03-07 01:04:12 +01:00
|
|
|
return Service::Audio::ResultInsufficientBuffer;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// invalidate samples_workbuffer DSP cache
|
|
|
|
|
|
|
|
auto voice_infos{allocator.Allocate<VoiceInfo>(params.voices, 0x10)};
|
|
|
|
for (auto& voice_info : voice_infos) {
|
|
|
|
std::construct_at<VoiceInfo>(&voice_info);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (voice_infos.empty()) {
|
2023-03-07 01:04:12 +01:00
|
|
|
return Service::Audio::ResultInsufficientBuffer;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
auto sorted_voice_infos{allocator.Allocate<VoiceInfo*>(params.voices, 0x10)};
|
|
|
|
if (sorted_voice_infos.empty()) {
|
2023-03-07 01:04:12 +01:00
|
|
|
return Service::Audio::ResultInsufficientBuffer;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
std::memset(sorted_voice_infos.data(), 0, sorted_voice_infos.size_bytes());
|
|
|
|
|
|
|
|
auto voice_channel_resources{allocator.Allocate<VoiceChannelResource>(params.voices, 0x10)};
|
|
|
|
u32 i{0};
|
|
|
|
for (auto& voice_channel_resource : voice_channel_resources) {
|
|
|
|
std::construct_at<VoiceChannelResource>(&voice_channel_resource, i++);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (voice_channel_resources.empty()) {
|
2023-03-07 01:04:12 +01:00
|
|
|
return Service::Audio::ResultInsufficientBuffer;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
auto voice_cpu_states{allocator.Allocate<VoiceState>(params.voices, 0x10)};
|
|
|
|
if (voice_cpu_states.empty()) {
|
2023-03-07 01:04:12 +01:00
|
|
|
return Service::Audio::ResultInsufficientBuffer;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
for (auto& voice_state : voice_cpu_states) {
|
|
|
|
voice_state = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
auto mix_infos{allocator.Allocate<MixInfo>(params.sub_mixes + 1, 0x10)};
|
|
|
|
|
|
|
|
if (mix_infos.empty()) {
|
2023-03-07 01:04:12 +01:00
|
|
|
return Service::Audio::ResultInsufficientBuffer;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
u32 effect_process_order_count{0};
|
|
|
|
std::span<s32> effect_process_order_buffer{};
|
|
|
|
|
|
|
|
if (params.effects > 0) {
|
|
|
|
effect_process_order_count = params.effects * (params.sub_mixes + 1);
|
|
|
|
effect_process_order_buffer = allocator.Allocate<s32>(effect_process_order_count, 0x10);
|
|
|
|
if (effect_process_order_buffer.empty()) {
|
2023-03-07 01:04:12 +01:00
|
|
|
return Service::Audio::ResultInsufficientBuffer;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
i = 0;
|
|
|
|
for (auto& mix_info : mix_infos) {
|
|
|
|
std::construct_at<MixInfo>(
|
|
|
|
&mix_info, effect_process_order_buffer.subspan(i * params.effects, params.effects),
|
|
|
|
params.effects, this->behavior);
|
|
|
|
i++;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto sorted_mix_infos{allocator.Allocate<MixInfo*>(params.sub_mixes + 1, 0x10)};
|
|
|
|
if (sorted_mix_infos.empty()) {
|
2023-03-07 01:04:12 +01:00
|
|
|
return Service::Audio::ResultInsufficientBuffer;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
std::memset(sorted_mix_infos.data(), 0, sorted_mix_infos.size_bytes());
|
|
|
|
|
|
|
|
if (behavior.IsSplitterSupported()) {
|
|
|
|
u64 node_state_size{NodeStates::GetWorkBufferSize(params.sub_mixes + 1)};
|
|
|
|
u64 edge_matrix_size{EdgeMatrix::GetWorkBufferSize(params.sub_mixes + 1)};
|
|
|
|
|
|
|
|
auto node_states_workbuffer{allocator.Allocate<u8>(node_state_size, 1)};
|
|
|
|
auto edge_matrix_workbuffer{allocator.Allocate<u8>(edge_matrix_size, 1)};
|
|
|
|
|
|
|
|
if (node_states_workbuffer.empty() || edge_matrix_workbuffer.size() == 0) {
|
2023-03-07 01:04:12 +01:00
|
|
|
return Service::Audio::ResultInsufficientBuffer;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
mix_context.Initialize(sorted_mix_infos, mix_infos, params.sub_mixes + 1,
|
|
|
|
effect_process_order_buffer, effect_process_order_count,
|
|
|
|
node_states_workbuffer, node_state_size, edge_matrix_workbuffer,
|
|
|
|
edge_matrix_size);
|
|
|
|
} else {
|
|
|
|
mix_context.Initialize(sorted_mix_infos, mix_infos, params.sub_mixes + 1,
|
|
|
|
effect_process_order_buffer, effect_process_order_count, {}, 0, {},
|
|
|
|
0);
|
|
|
|
}
|
|
|
|
|
|
|
|
upsampler_manager = allocator.Allocate<UpsamplerManager>(1, 0x10).data();
|
|
|
|
if (upsampler_manager == nullptr) {
|
2023-03-07 01:04:12 +01:00
|
|
|
return Service::Audio::ResultInsufficientBuffer;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
memory_pool_workbuffer = allocator.Allocate<MemoryPoolInfo>(memory_pool_count, 0x10);
|
|
|
|
for (auto& memory_pool : memory_pool_workbuffer) {
|
|
|
|
std::construct_at<MemoryPoolInfo>(&memory_pool, MemoryPoolInfo::Location::DSP);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (memory_pool_workbuffer.empty() && memory_pool_count > 0) {
|
2023-03-07 01:04:12 +01:00
|
|
|
return Service::Audio::ResultInsufficientBuffer;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (!splitter_context.Initialize(behavior, params, allocator)) {
|
2023-03-07 01:04:12 +01:00
|
|
|
return Service::Audio::ResultInsufficientBuffer;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
std::span<EffectResultState> effect_result_states_cpu{};
|
|
|
|
if (behavior.IsEffectInfoVersion2Supported() && params.effects > 0) {
|
|
|
|
effect_result_states_cpu = allocator.Allocate<EffectResultState>(params.effects, 0x10);
|
|
|
|
if (effect_result_states_cpu.empty()) {
|
2023-03-07 01:04:12 +01:00
|
|
|
return Service::Audio::ResultInsufficientBuffer;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
std::memset(effect_result_states_cpu.data(), 0, effect_result_states_cpu.size_bytes());
|
|
|
|
}
|
|
|
|
|
|
|
|
allocator.Align(0x40);
|
|
|
|
|
|
|
|
unk_2B0 = allocator.GetSize() - allocator.GetCurrentOffset();
|
|
|
|
unk_2A8 = {&workbuffer[allocator.GetCurrentOffset()], unk_2B0};
|
|
|
|
|
|
|
|
upsampler_infos = allocator.Allocate<UpsamplerInfo>(upsampler_count, 0x40);
|
|
|
|
for (auto& upsampler_info : upsampler_infos) {
|
|
|
|
std::construct_at<UpsamplerInfo>(&upsampler_info);
|
|
|
|
}
|
|
|
|
|
|
|
|
std::construct_at<UpsamplerManager>(upsampler_manager, upsampler_count, upsampler_infos,
|
|
|
|
upsampler_workbuffer);
|
|
|
|
|
|
|
|
if (upsampler_infos.empty()) {
|
2023-03-07 01:04:12 +01:00
|
|
|
return Service::Audio::ResultInsufficientBuffer;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
auto effect_infos{allocator.Allocate<EffectInfoBase>(params.effects, 0x40)};
|
|
|
|
for (auto& effect_info : effect_infos) {
|
|
|
|
std::construct_at<EffectInfoBase>(&effect_info);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (effect_infos.empty() && params.effects > 0) {
|
2023-03-07 01:04:12 +01:00
|
|
|
return Service::Audio::ResultInsufficientBuffer;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
std::span<EffectResultState> effect_result_states_dsp{};
|
|
|
|
if (behavior.IsEffectInfoVersion2Supported() && params.effects > 0) {
|
|
|
|
effect_result_states_dsp = allocator.Allocate<EffectResultState>(params.effects, 0x40);
|
|
|
|
if (effect_result_states_dsp.empty()) {
|
2023-03-07 01:04:12 +01:00
|
|
|
return Service::Audio::ResultInsufficientBuffer;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
std::memset(effect_result_states_dsp.data(), 0, effect_result_states_dsp.size_bytes());
|
|
|
|
}
|
|
|
|
|
|
|
|
effect_context.Initialize(effect_infos, params.effects, effect_result_states_cpu,
|
|
|
|
effect_result_states_dsp, effect_result_states_dsp.size());
|
|
|
|
|
|
|
|
auto sinks{allocator.Allocate<SinkInfoBase>(params.sinks, 0x10)};
|
|
|
|
for (auto& sink : sinks) {
|
|
|
|
std::construct_at<SinkInfoBase>(&sink);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (sinks.empty()) {
|
2023-03-07 01:04:12 +01:00
|
|
|
return Service::Audio::ResultInsufficientBuffer;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
sink_context.Initialize(sinks, params.sinks);
|
|
|
|
|
|
|
|
auto voice_dsp_states{allocator.Allocate<VoiceState>(params.voices, 0x40)};
|
|
|
|
if (voice_dsp_states.empty()) {
|
2023-03-07 01:04:12 +01:00
|
|
|
return Service::Audio::ResultInsufficientBuffer;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
for (auto& voice_state : voice_dsp_states) {
|
|
|
|
voice_state = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
voice_context.Initialize(sorted_voice_infos, voice_infos, voice_channel_resources,
|
|
|
|
voice_cpu_states, voice_dsp_states, params.voices);
|
|
|
|
|
|
|
|
if (params.perf_frames > 0) {
|
|
|
|
const auto perf_workbuffer_size{
|
|
|
|
PerformanceManager::GetRequiredBufferSizeForPerformanceMetricsPerFrame(behavior,
|
|
|
|
params) *
|
|
|
|
(params.perf_frames + 1) +
|
|
|
|
0xC};
|
|
|
|
performance_workbuffer = allocator.Allocate<u8>(perf_workbuffer_size, 0x40);
|
|
|
|
if (performance_workbuffer.empty()) {
|
2023-03-07 01:04:12 +01:00
|
|
|
return Service::Audio::ResultInsufficientBuffer;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
std::memset(performance_workbuffer.data(), 0, performance_workbuffer.size_bytes());
|
|
|
|
performance_manager.Initialize(performance_workbuffer, performance_workbuffer.size_bytes(),
|
|
|
|
params, behavior, memory_pool_info);
|
|
|
|
}
|
|
|
|
|
|
|
|
render_time_limit_percent = 100;
|
|
|
|
drop_voice = params.voice_drop_enabled && params.execution_mode == ExecutionMode::Auto;
|
2022-10-19 06:38:12 +02:00
|
|
|
drop_voice_param = 1.0f;
|
|
|
|
num_voices_dropped = 0;
|
2022-07-17 00:48:45 +02:00
|
|
|
|
|
|
|
allocator.Align(0x40);
|
|
|
|
command_workbuffer_size = allocator.GetRemainingSize();
|
|
|
|
command_workbuffer = allocator.Allocate<u8>(command_workbuffer_size, 0x40);
|
|
|
|
if (command_workbuffer.empty()) {
|
2023-03-07 01:04:12 +01:00
|
|
|
return Service::Audio::ResultInsufficientBuffer;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
command_buffer_size = 0;
|
|
|
|
reset_command_buffers = true;
|
|
|
|
|
|
|
|
// nn::audio::dsp::FlushDataCache(transferMemory, transferMemorySize);
|
|
|
|
|
|
|
|
if (behavior.IsCommandProcessingTimeEstimatorVersion5Supported()) {
|
|
|
|
command_processing_time_estimator =
|
|
|
|
std::make_unique<CommandProcessingTimeEstimatorVersion5>(sample_count,
|
|
|
|
mix_buffer_count);
|
|
|
|
} else if (behavior.IsCommandProcessingTimeEstimatorVersion4Supported()) {
|
|
|
|
command_processing_time_estimator =
|
|
|
|
std::make_unique<CommandProcessingTimeEstimatorVersion4>(sample_count,
|
|
|
|
mix_buffer_count);
|
|
|
|
} else if (behavior.IsCommandProcessingTimeEstimatorVersion3Supported()) {
|
|
|
|
command_processing_time_estimator =
|
|
|
|
std::make_unique<CommandProcessingTimeEstimatorVersion3>(sample_count,
|
|
|
|
mix_buffer_count);
|
|
|
|
} else if (behavior.IsCommandProcessingTimeEstimatorVersion2Supported()) {
|
|
|
|
command_processing_time_estimator =
|
|
|
|
std::make_unique<CommandProcessingTimeEstimatorVersion2>(sample_count,
|
|
|
|
mix_buffer_count);
|
|
|
|
} else {
|
|
|
|
command_processing_time_estimator =
|
|
|
|
std::make_unique<CommandProcessingTimeEstimatorVersion1>(sample_count,
|
|
|
|
mix_buffer_count);
|
|
|
|
}
|
|
|
|
|
|
|
|
initialized = true;
|
|
|
|
return ResultSuccess;
|
|
|
|
}
|
|
|
|
|
|
|
|
void System::Finalize() {
|
|
|
|
if (!initialized) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (active) {
|
|
|
|
Stop();
|
|
|
|
}
|
|
|
|
|
|
|
|
applet_resource_user_id = 0;
|
|
|
|
|
|
|
|
PoolMapper pool_mapper(process_handle, false);
|
|
|
|
pool_mapper.Unmap(memory_pool_info);
|
|
|
|
|
|
|
|
if (process_handle) {
|
|
|
|
pool_mapper.ClearUseState(memory_pool_workbuffer, memory_pool_count);
|
|
|
|
for (auto& memory_pool : memory_pool_workbuffer) {
|
|
|
|
if (memory_pool.IsMapped()) {
|
|
|
|
pool_mapper.Unmap(memory_pool);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// dsp::ProcessCleanup
|
|
|
|
// close handle
|
|
|
|
}
|
|
|
|
initialized = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void System::Start() {
|
|
|
|
std::scoped_lock l{lock};
|
|
|
|
frames_elapsed = 0;
|
|
|
|
state = State::Started;
|
|
|
|
active = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
void System::Stop() {
|
|
|
|
{
|
|
|
|
std::scoped_lock l{lock};
|
|
|
|
state = State::Stopped;
|
|
|
|
active = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (execution_mode == ExecutionMode::Auto) {
|
2023-05-01 01:27:12 +02:00
|
|
|
terminate_event.Wait();
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Result System::Update(std::span<const u8> input, std::span<u8> performance, std::span<u8> output) {
|
|
|
|
std::scoped_lock l{lock};
|
|
|
|
|
2023-08-31 16:09:15 +02:00
|
|
|
const auto start_time{core.CoreTiming().GetGlobalTimeNs().count()};
|
2023-05-23 15:45:54 +02:00
|
|
|
std::memset(output.data(), 0, output.size());
|
2022-07-17 00:48:45 +02:00
|
|
|
|
|
|
|
InfoUpdater info_updater(input, output, process_handle, behavior);
|
|
|
|
|
|
|
|
auto result{info_updater.UpdateBehaviorInfo(behavior)};
|
|
|
|
if (result.IsError()) {
|
|
|
|
LOG_ERROR(Service_Audio, "Failed to update BehaviorInfo!");
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
result = info_updater.UpdateMemoryPools(memory_pool_workbuffer, memory_pool_count);
|
|
|
|
if (result.IsError()) {
|
|
|
|
LOG_ERROR(Service_Audio, "Failed to update MemoryPools!");
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
result = info_updater.UpdateVoiceChannelResources(voice_context);
|
|
|
|
if (result.IsError()) {
|
|
|
|
LOG_ERROR(Service_Audio, "Failed to update VoiceChannelResources!");
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
result = info_updater.UpdateVoices(voice_context, memory_pool_workbuffer, memory_pool_count);
|
|
|
|
if (result.IsError()) {
|
|
|
|
LOG_ERROR(Service_Audio, "Failed to update Voices!");
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
result = info_updater.UpdateEffects(effect_context, active, memory_pool_workbuffer,
|
|
|
|
memory_pool_count);
|
|
|
|
if (result.IsError()) {
|
|
|
|
LOG_ERROR(Service_Audio, "Failed to update Effects!");
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (behavior.IsSplitterSupported()) {
|
|
|
|
result = info_updater.UpdateSplitterInfo(splitter_context);
|
|
|
|
if (result.IsError()) {
|
|
|
|
LOG_ERROR(Service_Audio, "Failed to update SplitterInfo!");
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
result =
|
|
|
|
info_updater.UpdateMixes(mix_context, mix_buffer_count, effect_context, splitter_context);
|
|
|
|
if (result.IsError()) {
|
|
|
|
LOG_ERROR(Service_Audio, "Failed to update Mixes!");
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
result = info_updater.UpdateSinks(sink_context, memory_pool_workbuffer, memory_pool_count);
|
|
|
|
if (result.IsError()) {
|
|
|
|
LOG_ERROR(Service_Audio, "Failed to update Sinks!");
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
PerformanceManager* perf_manager{nullptr};
|
|
|
|
if (performance_manager.IsInitialized()) {
|
|
|
|
perf_manager = &performance_manager;
|
|
|
|
}
|
|
|
|
|
|
|
|
result =
|
|
|
|
info_updater.UpdatePerformanceBuffer(performance, performance.size_bytes(), perf_manager);
|
|
|
|
if (result.IsError()) {
|
|
|
|
LOG_ERROR(Service_Audio, "Failed to update PerformanceBuffer!");
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
result = info_updater.UpdateErrorInfo(behavior);
|
|
|
|
if (result.IsError()) {
|
|
|
|
LOG_ERROR(Service_Audio, "Failed to update ErrorInfo!");
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (behavior.IsElapsedFrameCountSupported()) {
|
|
|
|
result = info_updater.UpdateRendererInfo(frames_elapsed);
|
|
|
|
if (result.IsError()) {
|
|
|
|
LOG_ERROR(Service_Audio, "Failed to update RendererInfo!");
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
result = info_updater.CheckConsumedSize();
|
|
|
|
if (result.IsError()) {
|
|
|
|
LOG_ERROR(Service_Audio, "Invalid consume size!");
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2022-10-13 02:26:04 +02:00
|
|
|
adsp_rendered_event->Clear();
|
2022-07-17 00:48:45 +02:00
|
|
|
num_times_updated++;
|
|
|
|
|
2023-08-31 16:09:15 +02:00
|
|
|
const auto end_time{core.CoreTiming().GetGlobalTimeNs().count()};
|
2022-07-17 00:48:45 +02:00
|
|
|
ticks_spent_updating += end_time - start_time;
|
|
|
|
|
|
|
|
return ResultSuccess;
|
|
|
|
}
|
|
|
|
|
|
|
|
u32 System::GetRenderingTimeLimit() const {
|
|
|
|
return render_time_limit_percent;
|
|
|
|
}
|
|
|
|
|
2022-10-19 06:38:12 +02:00
|
|
|
void System::SetRenderingTimeLimit(u32 limit) {
|
2022-07-17 00:48:45 +02:00
|
|
|
render_time_limit_percent = limit;
|
|
|
|
}
|
|
|
|
|
|
|
|
u32 System::GetSessionId() const {
|
|
|
|
return session_id;
|
|
|
|
}
|
|
|
|
|
|
|
|
u32 System::GetSampleRate() const {
|
|
|
|
return sample_rate;
|
|
|
|
}
|
|
|
|
|
|
|
|
u32 System::GetSampleCount() const {
|
|
|
|
return sample_count;
|
|
|
|
}
|
|
|
|
|
|
|
|
u32 System::GetMixBufferCount() const {
|
|
|
|
return mix_buffer_count;
|
|
|
|
}
|
|
|
|
|
|
|
|
ExecutionMode System::GetExecutionMode() const {
|
|
|
|
return execution_mode;
|
|
|
|
}
|
|
|
|
|
|
|
|
u32 System::GetRenderingDevice() const {
|
|
|
|
return render_device;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool System::IsActive() const {
|
|
|
|
return active;
|
|
|
|
}
|
|
|
|
|
|
|
|
void System::SendCommandToDsp() {
|
|
|
|
std::scoped_lock l{lock};
|
|
|
|
|
|
|
|
if (initialized) {
|
|
|
|
if (active) {
|
|
|
|
terminate_event.Reset();
|
2023-08-31 16:09:15 +02:00
|
|
|
const auto remaining_command_count{audio_renderer.GetRemainCommandCount(session_id)};
|
2022-07-17 00:48:45 +02:00
|
|
|
u64 command_size{0};
|
|
|
|
|
|
|
|
if (remaining_command_count) {
|
|
|
|
adsp_behind = true;
|
|
|
|
command_size = command_buffer_size;
|
|
|
|
} else {
|
|
|
|
command_size = GenerateCommand(command_workbuffer, command_workbuffer_size);
|
|
|
|
}
|
|
|
|
|
|
|
|
auto translated_addr{
|
|
|
|
memory_pool_info.Translate(CpuAddr(command_workbuffer.data()), command_size)};
|
|
|
|
|
|
|
|
auto time_limit_percent{70.0f};
|
|
|
|
if (behavior.IsAudioRendererProcessingTimeLimit80PercentSupported()) {
|
|
|
|
time_limit_percent = 80.0f;
|
|
|
|
} else if (behavior.IsAudioRendererProcessingTimeLimit75PercentSupported()) {
|
|
|
|
time_limit_percent = 75.0f;
|
|
|
|
} else {
|
|
|
|
// result ignored and 70 is used anyway
|
|
|
|
behavior.IsAudioRendererProcessingTimeLimit70PercentSupported();
|
|
|
|
time_limit_percent = 70.0f;
|
|
|
|
}
|
|
|
|
|
2023-09-07 21:53:48 +02:00
|
|
|
auto time_limit{
|
|
|
|
static_cast<u64>((time_limit_percent / 100) * 2'880'000.0 *
|
|
|
|
(static_cast<f32>(render_time_limit_percent) / 100.0f))};
|
|
|
|
audio_renderer.SetCommandBuffer(session_id, translated_addr, command_size, time_limit,
|
|
|
|
applet_resource_user_id, reset_command_buffers);
|
2022-07-17 00:48:45 +02:00
|
|
|
reset_command_buffers = false;
|
|
|
|
command_buffer_size = command_size;
|
|
|
|
if (remaining_command_count == 0) {
|
2022-10-13 02:26:04 +02:00
|
|
|
adsp_rendered_event->Signal();
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
} else {
|
2023-08-31 16:09:15 +02:00
|
|
|
audio_renderer.ClearRemainCommandCount(session_id);
|
2022-07-17 00:48:45 +02:00
|
|
|
terminate_event.Set();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
u64 System::GenerateCommand(std::span<u8> in_command_buffer,
|
2022-10-19 06:38:12 +02:00
|
|
|
[[maybe_unused]] u64 command_buffer_size_) {
|
2022-07-17 00:48:45 +02:00
|
|
|
PoolMapper::ClearUseState(memory_pool_workbuffer, memory_pool_count);
|
2023-08-31 16:09:15 +02:00
|
|
|
const auto start_time{core.CoreTiming().GetGlobalTimeNs().count()};
|
2022-07-17 00:48:45 +02:00
|
|
|
|
|
|
|
auto command_list_header{reinterpret_cast<CommandListHeader*>(in_command_buffer.data())};
|
|
|
|
|
|
|
|
command_list_header->buffer_count = static_cast<s16>(voice_channels + mix_buffer_count);
|
|
|
|
command_list_header->sample_count = sample_count;
|
|
|
|
command_list_header->sample_rate = sample_rate;
|
|
|
|
command_list_header->samples_buffer = samples_workbuffer;
|
|
|
|
|
|
|
|
const auto performance_initialized{performance_manager.IsInitialized()};
|
|
|
|
if (performance_initialized) {
|
|
|
|
performance_manager.TapFrame(adsp_behind, num_voices_dropped, render_start_tick);
|
|
|
|
adsp_behind = false;
|
|
|
|
num_voices_dropped = 0;
|
|
|
|
render_start_tick = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
s8 channel_count{2};
|
|
|
|
if (execution_mode == ExecutionMode::Auto) {
|
|
|
|
const auto& sink{core.AudioCore().GetOutputSink()};
|
|
|
|
channel_count = static_cast<s8>(sink.GetDeviceChannels());
|
|
|
|
}
|
|
|
|
|
|
|
|
AudioRendererSystemContext render_context{
|
|
|
|
.session_id{session_id},
|
|
|
|
.channels{channel_count},
|
|
|
|
.mix_buffer_count{mix_buffer_count},
|
|
|
|
.behavior{&behavior},
|
|
|
|
.depop_buffer{depop_buffer},
|
|
|
|
.upsampler_manager{upsampler_manager},
|
|
|
|
.memory_pool_info{&memory_pool_info},
|
|
|
|
};
|
|
|
|
|
|
|
|
CommandBuffer command_buffer{
|
|
|
|
.command_list{in_command_buffer},
|
|
|
|
.sample_count{sample_count},
|
|
|
|
.sample_rate{sample_rate},
|
|
|
|
.size{sizeof(CommandListHeader)},
|
|
|
|
.count{0},
|
|
|
|
.estimated_process_time{0},
|
|
|
|
.memory_pool{&memory_pool_info},
|
|
|
|
.time_estimator{command_processing_time_estimator.get()},
|
|
|
|
.behavior{&behavior},
|
|
|
|
};
|
|
|
|
|
|
|
|
PerformanceManager* perf_manager{nullptr};
|
|
|
|
if (performance_initialized) {
|
|
|
|
perf_manager = &performance_manager;
|
|
|
|
}
|
|
|
|
|
|
|
|
CommandGenerator command_generator{command_buffer, *command_list_header, render_context,
|
|
|
|
voice_context, mix_context, effect_context,
|
|
|
|
sink_context, splitter_context, perf_manager};
|
|
|
|
|
|
|
|
voice_context.SortInfo();
|
|
|
|
|
2022-10-19 06:38:12 +02:00
|
|
|
const auto start_estimated_time{drop_voice_param *
|
|
|
|
static_cast<f32>(command_buffer.estimated_process_time)};
|
2022-07-17 00:48:45 +02:00
|
|
|
|
|
|
|
command_generator.GenerateVoiceCommands();
|
|
|
|
command_generator.GenerateSubMixCommands();
|
|
|
|
command_generator.GenerateFinalMixCommands();
|
|
|
|
command_generator.GenerateSinkCommands();
|
|
|
|
|
|
|
|
if (drop_voice) {
|
|
|
|
f32 time_limit_percent{70.0f};
|
|
|
|
if (render_context.behavior->IsAudioRendererProcessingTimeLimit80PercentSupported()) {
|
|
|
|
time_limit_percent = 80.0f;
|
|
|
|
} else if (render_context.behavior
|
|
|
|
->IsAudioRendererProcessingTimeLimit75PercentSupported()) {
|
|
|
|
time_limit_percent = 75.0f;
|
|
|
|
} else {
|
|
|
|
// result is ignored
|
|
|
|
render_context.behavior->IsAudioRendererProcessingTimeLimit70PercentSupported();
|
|
|
|
time_limit_percent = 70.0f;
|
|
|
|
}
|
2022-10-19 06:38:12 +02:00
|
|
|
|
|
|
|
const auto end_estimated_time{drop_voice_param *
|
|
|
|
static_cast<f32>(command_buffer.estimated_process_time)};
|
|
|
|
const auto estimated_time{start_estimated_time - end_estimated_time};
|
|
|
|
|
2022-07-17 00:48:45 +02:00
|
|
|
const auto time_limit{static_cast<u32>(
|
2022-10-19 06:38:12 +02:00
|
|
|
estimated_time + (((time_limit_percent / 100.0f) * 2'880'000.0) *
|
|
|
|
(static_cast<f32>(render_time_limit_percent) / 100.0f)))};
|
|
|
|
num_voices_dropped =
|
|
|
|
DropVoices(command_buffer, static_cast<u32>(start_estimated_time), time_limit);
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
command_list_header->buffer_size = command_buffer.size;
|
|
|
|
command_list_header->command_count = command_buffer.count;
|
|
|
|
|
|
|
|
voice_context.UpdateStateByDspShared();
|
|
|
|
|
|
|
|
if (render_context.behavior->IsEffectInfoVersion2Supported()) {
|
|
|
|
effect_context.UpdateStateByDspShared();
|
|
|
|
}
|
|
|
|
|
2023-08-31 16:09:15 +02:00
|
|
|
const auto end_time{core.CoreTiming().GetGlobalTimeNs().count()};
|
2022-07-17 00:48:45 +02:00
|
|
|
total_ticks_elapsed += end_time - start_time;
|
|
|
|
num_command_lists_generated++;
|
2023-08-31 16:09:15 +02:00
|
|
|
render_start_tick = audio_renderer.GetRenderingStartTick(session_id);
|
2022-07-17 00:48:45 +02:00
|
|
|
frames_elapsed++;
|
|
|
|
|
|
|
|
return command_buffer.size;
|
|
|
|
}
|
|
|
|
|
2022-10-19 06:38:12 +02:00
|
|
|
f32 System::GetVoiceDropParameter() const {
|
|
|
|
return drop_voice_param;
|
|
|
|
}
|
|
|
|
|
|
|
|
void System::SetVoiceDropParameter(f32 voice_drop_) {
|
|
|
|
drop_voice_param = voice_drop_;
|
|
|
|
}
|
|
|
|
|
|
|
|
u32 System::DropVoices(CommandBuffer& command_buffer, u32 estimated_process_time, u32 time_limit) {
|
2022-07-17 00:48:45 +02:00
|
|
|
u32 i{0};
|
|
|
|
auto command_list{command_buffer.command_list.data() + sizeof(CommandListHeader)};
|
2022-10-19 06:38:12 +02:00
|
|
|
ICommand* cmd{nullptr};
|
2022-07-17 00:48:45 +02:00
|
|
|
|
2022-10-19 06:38:12 +02:00
|
|
|
// Find a first valid voice to drop
|
|
|
|
while (i < command_buffer.count) {
|
2022-07-17 00:48:45 +02:00
|
|
|
cmd = reinterpret_cast<ICommand*>(command_list);
|
2022-10-19 06:38:12 +02:00
|
|
|
if (cmd->type == CommandId::Performance ||
|
|
|
|
cmd->type == CommandId::DataSourcePcmInt16Version1 ||
|
|
|
|
cmd->type == CommandId::DataSourcePcmInt16Version2 ||
|
|
|
|
cmd->type == CommandId::DataSourcePcmFloatVersion1 ||
|
|
|
|
cmd->type == CommandId::DataSourcePcmFloatVersion2 ||
|
|
|
|
cmd->type == CommandId::DataSourceAdpcmVersion1 ||
|
|
|
|
cmd->type == CommandId::DataSourceAdpcmVersion2) {
|
2022-07-17 00:48:45 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
command_list += cmd->size;
|
2022-10-19 06:38:12 +02:00
|
|
|
i++;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if (cmd == nullptr || command_buffer.count == 0 || i >= command_buffer.count) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
auto voices_dropped{0};
|
|
|
|
while (i < command_buffer.count) {
|
|
|
|
const auto node_id{cmd->node_id};
|
|
|
|
const auto node_id_type{cmd->node_id >> 28};
|
2023-08-28 11:35:30 +02:00
|
|
|
const auto node_id_base{(cmd->node_id >> 16) & 0xFFF};
|
2022-07-17 00:48:45 +02:00
|
|
|
|
2022-10-19 06:38:12 +02:00
|
|
|
// If the new estimated process time falls below the limit, we're done dropping.
|
2022-07-17 00:48:45 +02:00
|
|
|
if (estimated_process_time <= time_limit) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (node_id_type != 1) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2022-10-19 06:38:12 +02:00
|
|
|
// Don't drop voices marked with the highest priority.
|
2022-07-17 00:48:45 +02:00
|
|
|
auto& voice_info{voice_context.GetInfo(node_id_base)};
|
|
|
|
if (voice_info.priority == HighestVoicePriority) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
voices_dropped++;
|
|
|
|
voice_info.voice_dropped = true;
|
|
|
|
|
2022-10-19 06:38:12 +02:00
|
|
|
// First iteration should drop the voice, and then iterate through all of the commands tied
|
|
|
|
// to the voice. We don't need reverb on a voice which we've just removed, for example.
|
|
|
|
// Depops can't be removed otherwise we'll introduce audio popping, and we don't
|
|
|
|
// remove perf commands. Lower the estimated time for each command dropped.
|
|
|
|
while (i < command_buffer.count && cmd->node_id == node_id) {
|
|
|
|
if (cmd->type == CommandId::DepopPrepare) {
|
|
|
|
cmd->enabled = true;
|
|
|
|
} else if (cmd->enabled && cmd->type != CommandId::Performance) {
|
|
|
|
cmd->enabled = false;
|
|
|
|
estimated_process_time -= static_cast<u32>(
|
|
|
|
drop_voice_param * static_cast<f32>(cmd->estimated_process_time));
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
2022-10-19 06:38:12 +02:00
|
|
|
command_list += cmd->size;
|
|
|
|
cmd = reinterpret_cast<ICommand*>(command_list);
|
|
|
|
i++;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
2022-10-19 06:38:12 +02:00
|
|
|
i++;
|
2022-07-17 00:48:45 +02:00
|
|
|
}
|
|
|
|
return voices_dropped;
|
|
|
|
}
|
|
|
|
|
2023-08-31 16:09:15 +02:00
|
|
|
} // namespace AudioCore::Renderer
|