1
0
mirror of https://git.suyu.dev/suyu/suyu synced 2025-09-04 02:16:31 -05:00

Initial commit

This commit is contained in:
Crimson-Hawk
2024-03-05 16:42:40 +08:00
commit f1e4595ebf
39576 changed files with 7006612 additions and 0 deletions

BIN
src/audio_core/.DS_Store vendored Normal file

Binary file not shown.

269
src/audio_core/CMakeLists.txt Executable file
View File

@@ -0,0 +1,269 @@
# SPDX-FileCopyrightText: 2018 yuzu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
add_library(audio_core STATIC
adsp/adsp.cpp
adsp/adsp.h
adsp/mailbox.h
adsp/apps/audio_renderer/audio_renderer.cpp
adsp/apps/audio_renderer/audio_renderer.h
adsp/apps/audio_renderer/command_buffer.h
adsp/apps/audio_renderer/command_list_processor.cpp
adsp/apps/audio_renderer/command_list_processor.h
adsp/apps/opus/opus_decoder.cpp
adsp/apps/opus/opus_decoder.h
adsp/apps/opus/opus_decode_object.cpp
adsp/apps/opus/opus_decode_object.h
adsp/apps/opus/opus_multistream_decode_object.cpp
adsp/apps/opus/opus_multistream_decode_object.h
adsp/apps/opus/shared_memory.h
audio_core.cpp
audio_core.h
audio_event.h
audio_event.cpp
audio_render_manager.cpp
audio_render_manager.h
audio_in_manager.cpp
audio_in_manager.h
audio_out_manager.cpp
audio_out_manager.h
audio_manager.cpp
audio_manager.h
common/audio_renderer_parameter.h
common/common.h
common/feature_support.h
common/wave_buffer.h
common/workbuffer_allocator.h
device/audio_buffer.h
device/audio_buffers.h
device/device_session.cpp
device/device_session.h
in/audio_in.cpp
in/audio_in.h
in/audio_in_system.cpp
in/audio_in_system.h
opus/hardware_opus.cpp
opus/hardware_opus.h
opus/decoder_manager.cpp
opus/decoder_manager.h
opus/decoder.cpp
opus/decoder.h
opus/parameters.h
out/audio_out.cpp
out/audio_out.h
out/audio_out_system.cpp
out/audio_out_system.h
precompiled_headers.h
renderer/audio_device.cpp
renderer/audio_device.h
renderer/audio_renderer.h
renderer/audio_renderer.cpp
renderer/behavior/behavior_info.cpp
renderer/behavior/behavior_info.h
renderer/behavior/info_updater.cpp
renderer/behavior/info_updater.h
renderer/command/data_source/adpcm.cpp
renderer/command/data_source/adpcm.h
renderer/command/data_source/decode.cpp
renderer/command/data_source/decode.h
renderer/command/data_source/pcm_float.cpp
renderer/command/data_source/pcm_float.h
renderer/command/data_source/pcm_int16.cpp
renderer/command/data_source/pcm_int16.h
renderer/command/effect/aux_.cpp
renderer/command/effect/aux_.h
renderer/command/effect/biquad_filter.cpp
renderer/command/effect/biquad_filter.h
renderer/command/effect/capture.cpp
renderer/command/effect/capture.h
renderer/command/effect/compressor.cpp
renderer/command/effect/compressor.h
renderer/command/effect/delay.cpp
renderer/command/effect/delay.h
renderer/command/effect/i3dl2_reverb.cpp
renderer/command/effect/i3dl2_reverb.h
renderer/command/effect/light_limiter.cpp
renderer/command/effect/light_limiter.h
renderer/command/effect/multi_tap_biquad_filter.cpp
renderer/command/effect/multi_tap_biquad_filter.h
renderer/command/effect/reverb.cpp
renderer/command/effect/reverb.h
renderer/command/mix/clear_mix.cpp
renderer/command/mix/clear_mix.h
renderer/command/mix/copy_mix.cpp
renderer/command/mix/copy_mix.h
renderer/command/mix/depop_for_mix_buffers.cpp
renderer/command/mix/depop_for_mix_buffers.h
renderer/command/mix/depop_prepare.cpp
renderer/command/mix/depop_prepare.h
renderer/command/mix/mix.cpp
renderer/command/mix/mix.h
renderer/command/mix/mix_ramp.cpp
renderer/command/mix/mix_ramp.h
renderer/command/mix/mix_ramp_grouped.cpp
renderer/command/mix/mix_ramp_grouped.h
renderer/command/mix/volume.cpp
renderer/command/mix/volume.h
renderer/command/mix/volume_ramp.cpp
renderer/command/mix/volume_ramp.h
renderer/command/performance/performance.cpp
renderer/command/performance/performance.h
renderer/command/resample/downmix_6ch_to_2ch.cpp
renderer/command/resample/downmix_6ch_to_2ch.h
renderer/command/resample/resample.h
renderer/command/resample/resample.cpp
renderer/command/resample/upsample.cpp
renderer/command/resample/upsample.h
renderer/command/sink/device.cpp
renderer/command/sink/device.h
renderer/command/sink/circular_buffer.cpp
renderer/command/sink/circular_buffer.h
renderer/command/command_buffer.cpp
renderer/command/command_buffer.h
renderer/command/command_generator.cpp
renderer/command/command_generator.h
renderer/command/command_list_header.h
renderer/command/command_processing_time_estimator.cpp
renderer/command/command_processing_time_estimator.h
renderer/command/commands.h
renderer/command/icommand.h
renderer/effect/aux_.cpp
renderer/effect/aux_.h
renderer/effect/biquad_filter.cpp
renderer/effect/biquad_filter.h
renderer/effect/buffer_mixer.cpp
renderer/effect/buffer_mixer.h
renderer/effect/capture.cpp
renderer/effect/capture.h
renderer/effect/compressor.cpp
renderer/effect/compressor.h
renderer/effect/delay.cpp
renderer/effect/delay.h
renderer/effect/effect_context.cpp
renderer/effect/effect_context.h
renderer/effect/effect_info_base.h
renderer/effect/effect_reset.h
renderer/effect/effect_result_state.h
renderer/effect/i3dl2.cpp
renderer/effect/i3dl2.h
renderer/effect/light_limiter.cpp
renderer/effect/light_limiter.h
renderer/effect/reverb.h
renderer/effect/reverb.cpp
renderer/mix/mix_context.cpp
renderer/mix/mix_context.h
renderer/mix/mix_info.cpp
renderer/mix/mix_info.h
renderer/memory/address_info.h
renderer/memory/memory_pool_info.cpp
renderer/memory/memory_pool_info.h
renderer/memory/pool_mapper.cpp
renderer/memory/pool_mapper.h
renderer/nodes/bit_array.h
renderer/nodes/edge_matrix.cpp
renderer/nodes/edge_matrix.h
renderer/nodes/node_states.cpp
renderer/nodes/node_states.h
renderer/performance/detail_aspect.cpp
renderer/performance/detail_aspect.h
renderer/performance/entry_aspect.cpp
renderer/performance/entry_aspect.h
renderer/performance/performance_detail.h
renderer/performance/performance_entry.h
renderer/performance/performance_entry_addresses.h
renderer/performance/performance_frame_header.h
renderer/performance/performance_manager.cpp
renderer/performance/performance_manager.h
renderer/sink/circular_buffer_sink_info.cpp
renderer/sink/circular_buffer_sink_info.h
renderer/sink/device_sink_info.cpp
renderer/sink/device_sink_info.h
renderer/sink/sink_context.cpp
renderer/sink/sink_context.h
renderer/sink/sink_info_base.cpp
renderer/sink/sink_info_base.h
renderer/splitter/splitter_context.cpp
renderer/splitter/splitter_context.h
renderer/splitter/splitter_destinations_data.cpp
renderer/splitter/splitter_destinations_data.h
renderer/splitter/splitter_info.cpp
renderer/splitter/splitter_info.h
renderer/system.cpp
renderer/system.h
renderer/system_manager.cpp
renderer/system_manager.h
renderer/upsampler/upsampler_info.h
renderer/upsampler/upsampler_manager.cpp
renderer/upsampler/upsampler_manager.h
renderer/upsampler/upsampler_state.h
renderer/voice/voice_channel_resource.h
renderer/voice/voice_context.cpp
renderer/voice/voice_context.h
renderer/voice/voice_info.cpp
renderer/voice/voice_info.h
renderer/voice/voice_state.h
sink/null_sink.h
sink/sink.h
sink/sink_details.cpp
sink/sink_details.h
sink/sink_stream.cpp
sink/sink_stream.h
)
if (MSVC)
target_compile_options(audio_core PRIVATE
/we4242 # 'identifier': conversion from 'type1' to 'type2', possible loss of data
/we4244 # 'conversion': conversion from 'type1' to 'type2', possible loss of data
/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
)
else()
target_compile_options(audio_core PRIVATE
-Werror=conversion
-Wno-sign-conversion
)
endif()
target_link_libraries(audio_core PUBLIC common core Opus::opus)
if (ARCHITECTURE_x86_64 OR ARCHITECTURE_arm64)
target_link_libraries(audio_core PRIVATE dynarmic::dynarmic)
endif()
if (ENABLE_CUBEB)
target_sources(audio_core PRIVATE
sink/cubeb_sink.cpp
sink/cubeb_sink.h
)
target_link_libraries(audio_core PRIVATE cubeb::cubeb)
target_compile_definitions(audio_core PRIVATE -DHAVE_CUBEB=1)
endif()
if (ENABLE_SDL2)
target_sources(audio_core PRIVATE
sink/sdl2_sink.cpp
sink/sdl2_sink.h
)
target_link_libraries(audio_core PRIVATE SDL2::SDL2)
target_compile_definitions(audio_core PRIVATE HAVE_SDL2)
endif()
if (ANDROID)
target_sources(audio_core PRIVATE
sink/oboe_sink.cpp
sink/oboe_sink.h
)
# FIXME: this port seems broken, it cannot be imported with find_package(oboe REQUIRED)
target_link_libraries(audio_core PRIVATE "${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/lib/liboboe.a")
target_compile_definitions(audio_core PRIVATE HAVE_OBOE)
endif()
if (YUZU_USE_PRECOMPILED_HEADERS)
target_precompile_headers(audio_core PRIVATE precompiled_headers.h)
endif()
create_target_directory_groups(audio_core)

27
src/audio_core/adsp/adsp.cpp Executable file
View File

@@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/adsp/adsp.h"
#include "core/core.h"
namespace AudioCore::ADSP {
ADSP::ADSP(Core::System& system, Sink::Sink& sink) {
audio_renderer = std::make_unique<AudioRenderer::AudioRenderer>(system, sink);
opus_decoder = std::make_unique<OpusDecoder::OpusDecoder>(system);
opus_decoder->Send(Direction::DSP, OpusDecoder::Message::Start);
if (opus_decoder->Receive(Direction::Host) != OpusDecoder::Message::StartOK) {
LOG_ERROR(Service_Audio, "OpusDecoder failed to initialize.");
return;
}
}
AudioRenderer::AudioRenderer& ADSP::AudioRenderer() {
return *audio_renderer.get();
}
OpusDecoder::OpusDecoder& ADSP::OpusDecoder() {
return *opus_decoder.get();
}
} // namespace AudioCore::ADSP

53
src/audio_core/adsp/adsp.h Executable file
View File

@@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "audio_core/adsp/apps/audio_renderer/audio_renderer.h"
#include "audio_core/adsp/apps/opus/opus_decoder.h"
#include "common/common_types.h"
namespace Core {
class System;
} // namespace Core
namespace AudioCore {
namespace Sink {
class Sink;
}
namespace ADSP {
/**
* Represents the ADSP embedded within the audio sysmodule.
* This is a 32-bit Linux4Tegra kernel from nVidia, which is launched with the sysmodule on boot.
*
* The kernel will run the apps you write for it, Nintendo have the following:
*
* Gmix - Responsible for mixing final audio and sending it out to hardware. This is last place all
* audio samples end up, and we skip it entirely, since we have very different backends and
* mixing is implicitly handled by the OS (but also due to lack of research/simplicity).
*
* AudioRenderer - Receives command lists generated by the audio render
* system on the host, processes them, and sends the samples to Gmix.
*
* OpusDecoder - Contains libopus, and decodes Opus audio packets into raw pcm data.
*
* Communication between the host and ADSP is done through mailboxes, and mapping of shared memory.
*/
class ADSP {
public:
explicit ADSP(Core::System& system, Sink::Sink& sink);
~ADSP() = default;
AudioRenderer::AudioRenderer& AudioRenderer();
OpusDecoder::OpusDecoder& OpusDecoder();
private:
/// AudioRenderer app
std::unique_ptr<AudioRenderer::AudioRenderer> audio_renderer{};
std::unique_ptr<OpusDecoder::OpusDecoder> opus_decoder{};
};
} // namespace ADSP
} // namespace AudioCore

View File

@@ -0,0 +1,230 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <array>
#include <chrono>
#include "audio_core/adsp/apps/audio_renderer/audio_renderer.h"
#include "audio_core/audio_core.h"
#include "audio_core/common/common.h"
#include "audio_core/sink/sink.h"
#include "common/logging/log.h"
#include "common/microprofile.h"
#include "common/thread.h"
#include "core/core.h"
#include "core/core_timing.h"
MICROPROFILE_DEFINE(Audio_Renderer, "Audio", "DSP_AudioRenderer", MP_RGB(60, 19, 97));
namespace AudioCore::ADSP::AudioRenderer {
AudioRenderer::AudioRenderer(Core::System& system_, Sink::Sink& sink_)
: system{system_}, sink{sink_} {}
AudioRenderer::~AudioRenderer() {
Stop();
}
void AudioRenderer::Start() {
CreateSinkStreams();
mailbox.Initialize(AppMailboxId::AudioRenderer);
main_thread = std::jthread([this](std::stop_token stop_token) { Main(stop_token); });
mailbox.Send(Direction::DSP, Message::InitializeOK);
if (mailbox.Receive(Direction::Host) != Message::InitializeOK) {
LOG_ERROR(Service_Audio, "Host Audio Renderer -- Failed to receive shutdown "
"message response from ADSP!");
return;
}
running = true;
}
void AudioRenderer::Stop() {
if (!running) {
return;
}
mailbox.Send(Direction::DSP, Message::Shutdown);
if (mailbox.Receive(Direction::Host) != Message::Shutdown) {
LOG_ERROR(Service_Audio, "Host Audio Renderer -- Failed to receive shutdown "
"message response from ADSP!");
}
main_thread.request_stop();
main_thread.join();
for (auto& stream : streams) {
if (stream) {
stream->Stop();
sink.CloseStream(stream);
stream = nullptr;
}
}
running = false;
}
void AudioRenderer::Signal() {
signalled_tick = system.CoreTiming().GetGlobalTimeNs().count();
Send(Direction::DSP, Message::Render);
}
void AudioRenderer::Wait() {
auto msg = Receive(Direction::Host);
if (msg != Message::RenderResponse) {
LOG_ERROR(Service_Audio,
"Did not receive the expected render response from the AudioRenderer! Expected "
"{}, got {}",
Message::RenderResponse, msg);
}
PostDSPClearCommandBuffer();
}
void AudioRenderer::Send(Direction dir, u32 message) {
mailbox.Send(dir, std::move(message));
}
u32 AudioRenderer::Receive(Direction dir) {
return mailbox.Receive(dir);
}
void AudioRenderer::SetCommandBuffer(s32 session_id, CpuAddr buffer, u64 size, u64 time_limit,
u64 applet_resource_user_id, Kernel::KProcess* process,
bool reset) noexcept {
command_buffers[session_id].buffer = buffer;
command_buffers[session_id].size = size;
command_buffers[session_id].time_limit = time_limit;
command_buffers[session_id].applet_resource_user_id = applet_resource_user_id;
command_buffers[session_id].process = process;
command_buffers[session_id].reset_buffer = reset;
}
void AudioRenderer::PostDSPClearCommandBuffer() noexcept {
for (auto& buffer : command_buffers) {
buffer.buffer = 0;
buffer.size = 0;
buffer.reset_buffer = false;
}
}
u32 AudioRenderer::GetRemainCommandCount(s32 session_id) const noexcept {
return command_buffers[session_id].remaining_command_count;
}
void AudioRenderer::ClearRemainCommandCount(s32 session_id) noexcept {
command_buffers[session_id].remaining_command_count = 0;
}
u64 AudioRenderer::GetRenderingStartTick(s32 session_id) const noexcept {
return (1000 * command_buffers[session_id].render_time_taken_us) + signalled_tick;
}
void AudioRenderer::CreateSinkStreams() {
u32 channels{sink.GetDeviceChannels()};
for (u32 i = 0; i < MaxRendererSessions; i++) {
std::string name{fmt::format("ADSP_RenderStream-{}", i)};
streams[i] =
sink.AcquireSinkStream(system, channels, name, ::AudioCore::Sink::StreamType::Render);
streams[i]->SetRingSize(4);
}
}
void AudioRenderer::Main(std::stop_token stop_token) {
static constexpr char name[]{"DSP_AudioRenderer_Main"};
MicroProfileOnThreadCreate(name);
Common::SetCurrentThreadName(name);
Common::SetCurrentThreadPriority(Common::ThreadPriority::High);
// TODO: Create buffer map/unmap thread + mailbox
// TODO: Create gMix devices, initialize them here
if (mailbox.Receive(Direction::DSP) != Message::InitializeOK) {
LOG_ERROR(Service_Audio,
"ADSP Audio Renderer -- Failed to receive initialize message from host!");
return;
}
mailbox.Send(Direction::Host, Message::InitializeOK);
// 0.12 seconds (2,304,000 / 19,200,000)
constexpr u64 max_process_time{2'304'000ULL};
while (!stop_token.stop_requested()) {
auto msg{mailbox.Receive(Direction::DSP)};
switch (msg) {
case Message::Shutdown:
mailbox.Send(Direction::Host, Message::Shutdown);
return;
case Message::Render: {
if (system.IsShuttingDown()) [[unlikely]] {
std::this_thread::sleep_for(std::chrono::milliseconds(5));
mailbox.Send(Direction::Host, Message::RenderResponse);
continue;
}
std::array<bool, MaxRendererSessions> buffers_reset{};
std::array<u64, MaxRendererSessions> render_times_taken{};
const auto start_time{system.CoreTiming().GetGlobalTimeUs().count()};
for (u32 index = 0; index < MaxRendererSessions; index++) {
auto& command_buffer{command_buffers[index]};
auto& command_list_processor{command_list_processors[index]};
// Check this buffer is valid, as it may not be used.
if (command_buffer.buffer != 0) {
// If there are no remaining commands (from the previous list),
// this is a new command list, initialize it.
if (command_buffer.remaining_command_count == 0) {
command_list_processor.Initialize(system, *command_buffer.process,
command_buffer.buffer,
command_buffer.size, streams[index]);
}
if (command_buffer.reset_buffer && !buffers_reset[index]) {
streams[index]->ClearQueue();
buffers_reset[index] = true;
}
u64 max_time{max_process_time};
if (index == 1 && command_buffer.applet_resource_user_id ==
command_buffers[0].applet_resource_user_id) {
max_time = max_process_time - render_times_taken[0];
if (render_times_taken[0] > max_process_time) {
max_time = 0;
}
}
max_time = std::min(command_buffer.time_limit, max_time);
command_list_processor.SetProcessTimeMax(max_time);
if (index == 0) {
streams[index]->WaitFreeSpace(stop_token);
}
// Process the command list
{
MICROPROFILE_SCOPE(Audio_Renderer);
render_times_taken[index] =
command_list_processor.Process(index) - start_time;
}
const auto end_time{system.CoreTiming().GetGlobalTimeUs().count()};
command_buffer.remaining_command_count =
command_list_processor.GetRemainingCommandCount();
command_buffer.render_time_taken_us = end_time - start_time;
}
}
mailbox.Send(Direction::Host, Message::RenderResponse);
} break;
default:
LOG_WARNING(Service_Audio,
"ADSP AudioRenderer received an invalid message, msg={:02X}!", msg);
break;
}
}
}
} // namespace AudioCore::ADSP::AudioRenderer

View File

@@ -0,0 +1,116 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <memory>
#include <thread>
#include "audio_core/adsp/apps/audio_renderer/command_buffer.h"
#include "audio_core/adsp/apps/audio_renderer/command_list_processor.h"
#include "audio_core/adsp/mailbox.h"
#include "common/common_types.h"
#include "common/polyfill_thread.h"
#include "common/reader_writer_queue.h"
#include "common/thread.h"
namespace Core {
class System;
} // namespace Core
namespace Kernel {
class KProcess;
}
namespace AudioCore {
namespace Sink {
class Sink;
}
namespace ADSP::AudioRenderer {
enum Message : u32 {
Invalid = 0,
MapUnmap_Map = 1,
MapUnmap_MapResponse = 2,
MapUnmap_Unmap = 3,
MapUnmap_UnmapResponse = 4,
MapUnmap_InvalidateCache = 5,
MapUnmap_InvalidateCacheResponse = 6,
MapUnmap_Shutdown = 7,
MapUnmap_ShutdownResponse = 8,
InitializeOK = 22,
RenderResponse = 32,
Render = 42,
Shutdown = 52,
};
/**
* The AudioRenderer application running on the ADSP.
*/
class AudioRenderer {
public:
explicit AudioRenderer(Core::System& system, Sink::Sink& sink);
~AudioRenderer();
/**
* Start the AudioRenderer.
*
* @param mailbox The mailbox to use for this session.
*/
void Start();
/**
* Stop the AudioRenderer.
*/
void Stop();
void Signal();
void Wait();
void Send(Direction dir, u32 message);
u32 Receive(Direction dir);
void SetCommandBuffer(s32 session_id, CpuAddr buffer, u64 size, u64 time_limit,
u64 applet_resource_user_id, Kernel::KProcess* process,
bool reset) noexcept;
u32 GetRemainCommandCount(s32 session_id) const noexcept;
void ClearRemainCommandCount(s32 session_id) noexcept;
u64 GetRenderingStartTick(s32 session_id) const noexcept;
private:
/**
* Main AudioRenderer thread, responsible for processing the command lists.
*/
void Main(std::stop_token stop_token);
/**
* Creates the streams which will receive the processed samples.
*/
void CreateSinkStreams();
void PostDSPClearCommandBuffer() noexcept;
/// Core system
Core::System& system;
/// The output sink the AudioRenderer will send samples to
Sink::Sink& sink;
/// The active mailbox
Mailbox mailbox;
/// Main thread
std::jthread main_thread{};
/// The current state
std::atomic<bool> running{};
/// Shared memory of input command buffers, set by host, read by DSP
std::array<CommandBuffer, MaxRendererSessions> command_buffers{};
/// The command lists to process
std::array<CommandListProcessor, MaxRendererSessions> command_list_processors{};
/// The streams which will receive the processed samples
std::array<Sink::SinkStream*, MaxRendererSessions> streams{};
/// CPU Tick when the DSP was signalled to process, uses time rather than tick
u64 signalled_tick{0};
};
} // namespace ADSP::AudioRenderer
} // namespace AudioCore

View File

@@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "audio_core/common/common.h"
#include "common/common_types.h"
namespace Kernel {
class KProcess;
}
namespace AudioCore::ADSP::AudioRenderer {
struct CommandBuffer {
// Set by the host
CpuAddr buffer{};
u64 size{};
u64 time_limit{};
u64 applet_resource_user_id{};
Kernel::KProcess* process{};
bool reset_buffer{};
// Set by the DSP
u32 remaining_command_count{};
u64 render_time_taken_us{};
};
} // namespace AudioCore::ADSP::AudioRenderer

View File

@@ -0,0 +1,104 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <string>
#include "audio_core/adsp/apps/audio_renderer/command_list_processor.h"
#include "audio_core/renderer/command/command_list_header.h"
#include "audio_core/renderer/command/commands.h"
#include "common/settings.h"
#include "core/core.h"
#include "core/core_timing.h"
#include "core/hle/kernel/k_process.h"
#include "core/memory.h"
namespace AudioCore::ADSP::AudioRenderer {
void CommandListProcessor::Initialize(Core::System& system_, Kernel::KProcess& process,
CpuAddr buffer, u64 size, Sink::SinkStream* stream_) {
system = &system_;
memory = &process.GetMemory();
stream = stream_;
header = reinterpret_cast<Renderer::CommandListHeader*>(buffer);
commands = reinterpret_cast<u8*>(buffer + sizeof(Renderer::CommandListHeader));
commands_buffer_size = size;
command_count = header->command_count;
sample_count = header->sample_count;
target_sample_rate = header->sample_rate;
mix_buffers = header->samples_buffer;
buffer_count = header->buffer_count;
processed_command_count = 0;
}
void CommandListProcessor::SetProcessTimeMax(const u64 time) {
max_process_time = time;
}
u32 CommandListProcessor::GetRemainingCommandCount() const {
return command_count - processed_command_count;
}
Sink::SinkStream* CommandListProcessor::GetOutputSinkStream() const {
return stream;
}
u64 CommandListProcessor::Process(u32 session_id) {
const auto start_time_{system->CoreTiming().GetGlobalTimeUs().count()};
const auto command_base{CpuAddr(commands)};
if (processed_command_count > 0) {
current_processing_time += start_time_ - end_time;
} else {
start_time = start_time_;
current_processing_time = 0;
}
std::string dump{fmt::format("\nSession {}\n", session_id)};
for (u32 index = 0; index < command_count; index++) {
auto& command{*reinterpret_cast<Renderer::ICommand*>(commands)};
if (command.magic != 0xCAFEBABE) {
LOG_ERROR(Service_Audio, "Command has invalid magic! Expected 0xCAFEBABE, got {:08X}",
command.magic);
return system->CoreTiming().GetGlobalTimeUs().count() - start_time_;
}
auto current_offset{CpuAddr(commands) - command_base};
if (current_offset + command.size > commands_buffer_size) {
LOG_ERROR(Service_Audio,
"Command exceeded command buffer, buffer size {:08X}, command ends at {:08X}",
commands_buffer_size,
CpuAddr(commands) + command.size - sizeof(Renderer::CommandListHeader));
return system->CoreTiming().GetGlobalTimeUs().count() - start_time_;
}
if (Settings::values.dump_audio_commands) {
command.Dump(*this, dump);
}
if (!command.Verify(*this)) {
break;
}
if (command.enabled) {
command.Process(*this);
} else {
dump += fmt::format("\tDisabled!\n");
}
processed_command_count++;
commands += command.size;
}
if (Settings::values.dump_audio_commands && dump != last_dump) {
LOG_WARNING(Service_Audio, "{}", dump);
last_dump = dump;
}
end_time = system->CoreTiming().GetGlobalTimeUs().count();
return end_time - start_time_;
}
} // namespace AudioCore::ADSP::AudioRenderer

View File

@@ -0,0 +1,117 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <span>
#include "audio_core/common/common.h"
#include "audio_core/renderer/command/command_list_header.h"
#include "common/common_types.h"
namespace Core {
namespace Memory {
class Memory;
}
class System;
} // namespace Core
namespace Kernel {
class KProcess;
}
namespace AudioCore {
namespace Sink {
class SinkStream;
}
namespace Renderer {
struct CommandListHeader;
}
namespace ADSP::AudioRenderer {
/**
* A processor for command lists given to the AudioRenderer.
*/
class CommandListProcessor {
public:
/**
* Initialize the processor.
*
* @param system - The core system.
* @param buffer - The command buffer to process.
* @param size - The size of the buffer.
* @param stream - The stream to be used for sending the samples.
*/
void Initialize(Core::System& system, Kernel::KProcess& process, CpuAddr buffer, u64 size,
Sink::SinkStream* stream);
/**
* Set the maximum processing time for this command list.
*
* @param time - The maximum process time.
*/
void SetProcessTimeMax(u64 time);
/**
* Get the remaining command count for this list.
*
* @return The remaining command count.
*/
u32 GetRemainingCommandCount() const;
/**
* Get the stream for this command list.
*
* @return The stream associated with this command list.
*/
Sink::SinkStream* GetOutputSinkStream() const;
/**
* Process the command list.
*
* @param session_id - Session ID for the commands being processed.
*
* @return The time taken to process.
*/
u64 Process(u32 session_id);
/// Core system
Core::System* system{};
/// Core memory
Core::Memory::Memory* memory{};
/// Stream for the processed samples
Sink::SinkStream* stream{};
/// Header info for this command list
Renderer::CommandListHeader* header{};
/// The command buffer
u8* commands{};
/// The command buffer size
u64 commands_buffer_size{};
/// The maximum processing time allotted
u64 max_process_time{};
/// The number of commands in the buffer
u32 command_count{};
/// The target sample count for output
u32 sample_count{};
/// The target sample rate for output
u32 target_sample_rate{};
/// The mixing buffers used by the commands
std::span<s32> mix_buffers{};
/// The number of mix buffers
u32 buffer_count{};
/// The number of processed commands so far
u32 processed_command_count{};
/// The processing start time of this list
u64 start_time{};
/// The current processing time for this list
u64 current_processing_time{};
/// The end processing time for this list
u64 end_time{};
/// Last command list string generated, used for dumping audio commands to console
std::string last_dump{};
};
} // namespace ADSP::AudioRenderer
} // namespace AudioCore

View File

@@ -0,0 +1,107 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/adsp/apps/opus/opus_decode_object.h"
#include "common/assert.h"
namespace AudioCore::ADSP::OpusDecoder {
namespace {
bool IsValidChannelCount(u32 channel_count) {
return channel_count == 1 || channel_count == 2;
}
} // namespace
u32 OpusDecodeObject::GetWorkBufferSize(u32 channel_count) {
if (!IsValidChannelCount(channel_count)) {
return 0;
}
return static_cast<u32>(sizeof(OpusDecodeObject)) + opus_decoder_get_size(channel_count);
}
OpusDecodeObject& OpusDecodeObject::Initialize(u64 buffer, u64 buffer2) {
auto* new_decoder = reinterpret_cast<OpusDecodeObject*>(buffer);
auto* comparison = reinterpret_cast<OpusDecodeObject*>(buffer2);
if (new_decoder->magic == DecodeObjectMagic) {
if (!new_decoder->initialized ||
(new_decoder->initialized && new_decoder->self == comparison)) {
new_decoder->state_valid = true;
}
} else {
new_decoder->initialized = false;
new_decoder->state_valid = true;
}
return *new_decoder;
}
s32 OpusDecodeObject::InitializeDecoder(u32 sample_rate, u32 channel_count) {
if (!state_valid) {
return OPUS_INVALID_STATE;
}
if (initialized) {
return OPUS_OK;
}
// Unfortunately libopus does not expose the OpusDecoder struct publicly, so we can't include
// it in this class. Nintendo does not allocate memory, which is why we have a workbuffer
// provided.
// We could use _create and have libopus allocate it for us, but then we have to separately
// track which decoder is being used between this and multistream in order to call the correct
// destroy from the host side.
// This is a bit cringe, but is safe as these objects are only ever initialized inside the given
// workbuffer, and GetWorkBufferSize will guarantee there's enough space to follow.
decoder = (LibOpusDecoder*)(this + 1);
s32 ret = opus_decoder_init(decoder, sample_rate, channel_count);
if (ret == OPUS_OK) {
magic = DecodeObjectMagic;
initialized = true;
state_valid = true;
self = this;
final_range = 0;
}
return ret;
}
s32 OpusDecodeObject::Shutdown() {
if (!state_valid) {
return OPUS_INVALID_STATE;
}
if (initialized) {
magic = 0x0;
initialized = false;
state_valid = false;
self = nullptr;
final_range = 0;
decoder = nullptr;
}
return OPUS_OK;
}
s32 OpusDecodeObject::ResetDecoder() {
return opus_decoder_ctl(decoder, OPUS_RESET_STATE);
}
s32 OpusDecodeObject::Decode(u32& out_sample_count, u64 output_data, u64 output_data_size,
u64 input_data, u64 input_data_size) {
ASSERT(initialized);
out_sample_count = 0;
if (!state_valid) {
return OPUS_INVALID_STATE;
}
auto ret_code_or_samples = opus_decode(
decoder, reinterpret_cast<const u8*>(input_data), static_cast<opus_int32>(input_data_size),
reinterpret_cast<opus_int16*>(output_data), static_cast<opus_int32>(output_data_size), 0);
if (ret_code_or_samples < OPUS_OK) {
return ret_code_or_samples;
}
out_sample_count = ret_code_or_samples;
return opus_decoder_ctl(decoder, OPUS_GET_FINAL_RANGE_REQUEST, &final_range);
}
} // namespace AudioCore::ADSP::OpusDecoder

View File

@@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <opus.h>
#include "common/common_types.h"
namespace AudioCore::ADSP::OpusDecoder {
using LibOpusDecoder = ::OpusDecoder;
static constexpr u32 DecodeObjectMagic = 0xDEADBEEF;
class OpusDecodeObject {
public:
static u32 GetWorkBufferSize(u32 channel_count);
static OpusDecodeObject& Initialize(u64 buffer, u64 buffer2);
s32 InitializeDecoder(u32 sample_rate, u32 channel_count);
s32 Shutdown();
s32 ResetDecoder();
s32 Decode(u32& out_sample_count, u64 output_data, u64 output_data_size, u64 input_data,
u64 input_data_size);
u32 GetFinalRange() const noexcept {
return final_range;
}
private:
u32 magic;
bool initialized;
bool state_valid;
OpusDecodeObject* self;
u32 final_range;
LibOpusDecoder* decoder;
};
static_assert(std::is_trivially_constructible_v<OpusDecodeObject>);
} // namespace AudioCore::ADSP::OpusDecoder

View File

@@ -0,0 +1,269 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <array>
#include <chrono>
#include "audio_core/adsp/apps/opus/opus_decode_object.h"
#include "audio_core/adsp/apps/opus/opus_multistream_decode_object.h"
#include "audio_core/adsp/apps/opus/shared_memory.h"
#include "audio_core/audio_core.h"
#include "audio_core/common/common.h"
#include "common/logging/log.h"
#include "common/microprofile.h"
#include "common/thread.h"
#include "core/core.h"
#include "core/core_timing.h"
MICROPROFILE_DEFINE(OpusDecoder, "Audio", "DSP_OpusDecoder", MP_RGB(60, 19, 97));
namespace AudioCore::ADSP::OpusDecoder {
namespace {
constexpr size_t OpusStreamCountMax = 255;
bool IsValidChannelCount(u32 channel_count) {
return channel_count == 1 || channel_count == 2;
}
bool IsValidMultiStreamChannelCount(u32 channel_count) {
return channel_count <= OpusStreamCountMax;
}
bool IsValidMultiStreamStreamCounts(s32 total_stream_count, s32 stereo_stream_count) {
return IsValidMultiStreamChannelCount(total_stream_count) && total_stream_count > 0 &&
stereo_stream_count >= 0 && stereo_stream_count <= total_stream_count;
}
} // namespace
OpusDecoder::OpusDecoder(Core::System& system_) : system{system_} {
init_thread = std::jthread([this](std::stop_token stop_token) { Init(stop_token); });
}
OpusDecoder::~OpusDecoder() {
if (!running) {
init_thread.request_stop();
return;
}
// Shutdown the thread
Send(Direction::DSP, Message::Shutdown);
auto msg = Receive(Direction::Host);
ASSERT_MSG(msg == Message::ShutdownOK, "Expected Opus shutdown code {}, got {}",
Message::ShutdownOK, msg);
main_thread.request_stop();
main_thread.join();
running = false;
}
void OpusDecoder::Send(Direction dir, u32 message) {
mailbox.Send(dir, std::move(message));
}
u32 OpusDecoder::Receive(Direction dir, std::stop_token stop_token) {
return mailbox.Receive(dir, stop_token);
}
void OpusDecoder::Init(std::stop_token stop_token) {
Common::SetCurrentThreadName("DSP_OpusDecoder_Init");
if (Receive(Direction::DSP, stop_token) != Message::Start) {
LOG_ERROR(Service_Audio,
"DSP OpusDecoder failed to receive Start message. Opus initialization failed.");
return;
}
main_thread = std::jthread([this](std::stop_token st) { Main(st); });
running = true;
Send(Direction::Host, Message::StartOK);
}
void OpusDecoder::Main(std::stop_token stop_token) {
Common::SetCurrentThreadName("DSP_OpusDecoder_Main");
while (!stop_token.stop_requested()) {
auto msg = Receive(Direction::DSP, stop_token);
switch (msg) {
case Shutdown:
Send(Direction::Host, Message::ShutdownOK);
return;
case GetWorkBufferSize: {
auto channel_count = static_cast<s32>(shared_memory->host_send_data[0]);
ASSERT(IsValidChannelCount(channel_count));
shared_memory->dsp_return_data[0] = OpusDecodeObject::GetWorkBufferSize(channel_count);
Send(Direction::Host, Message::GetWorkBufferSizeOK);
} break;
case InitializeDecodeObject: {
auto buffer = shared_memory->host_send_data[0];
auto buffer_size = shared_memory->host_send_data[1];
auto sample_rate = static_cast<s32>(shared_memory->host_send_data[2]);
auto channel_count = static_cast<s32>(shared_memory->host_send_data[3]);
ASSERT(sample_rate >= 0);
ASSERT(IsValidChannelCount(channel_count));
ASSERT(buffer_size >= OpusDecodeObject::GetWorkBufferSize(channel_count));
auto& decoder_object = OpusDecodeObject::Initialize(buffer, buffer);
shared_memory->dsp_return_data[0] =
decoder_object.InitializeDecoder(sample_rate, channel_count);
Send(Direction::Host, Message::InitializeDecodeObjectOK);
} break;
case ShutdownDecodeObject: {
auto buffer = shared_memory->host_send_data[0];
[[maybe_unused]] auto buffer_size = shared_memory->host_send_data[1];
auto& decoder_object = OpusDecodeObject::Initialize(buffer, buffer);
shared_memory->dsp_return_data[0] = decoder_object.Shutdown();
Send(Direction::Host, Message::ShutdownDecodeObjectOK);
} break;
case DecodeInterleaved: {
auto start_time = system.CoreTiming().GetGlobalTimeUs();
auto buffer = shared_memory->host_send_data[0];
auto input_data = shared_memory->host_send_data[1];
auto input_data_size = shared_memory->host_send_data[2];
auto output_data = shared_memory->host_send_data[3];
auto output_data_size = shared_memory->host_send_data[4];
auto final_range = static_cast<u32>(shared_memory->host_send_data[5]);
auto reset_requested = shared_memory->host_send_data[6];
u32 decoded_samples{0};
auto& decoder_object = OpusDecodeObject::Initialize(buffer, buffer);
s32 error_code{OPUS_OK};
if (reset_requested) {
error_code = decoder_object.ResetDecoder();
}
if (error_code == OPUS_OK) {
error_code = decoder_object.Decode(decoded_samples, output_data, output_data_size,
input_data, input_data_size);
}
if (error_code == OPUS_OK) {
if (final_range && decoder_object.GetFinalRange() != final_range) {
error_code = OPUS_INVALID_PACKET;
}
}
auto end_time = system.CoreTiming().GetGlobalTimeUs();
shared_memory->dsp_return_data[0] = error_code;
shared_memory->dsp_return_data[1] = decoded_samples;
shared_memory->dsp_return_data[2] = (end_time - start_time).count();
Send(Direction::Host, Message::DecodeInterleavedOK);
} break;
case MapMemory: {
[[maybe_unused]] auto buffer = shared_memory->host_send_data[0];
[[maybe_unused]] auto buffer_size = shared_memory->host_send_data[1];
Send(Direction::Host, Message::MapMemoryOK);
} break;
case UnmapMemory: {
[[maybe_unused]] auto buffer = shared_memory->host_send_data[0];
[[maybe_unused]] auto buffer_size = shared_memory->host_send_data[1];
Send(Direction::Host, Message::UnmapMemoryOK);
} break;
case GetWorkBufferSizeForMultiStream: {
auto total_stream_count = static_cast<s32>(shared_memory->host_send_data[0]);
auto stereo_stream_count = static_cast<s32>(shared_memory->host_send_data[1]);
ASSERT(IsValidMultiStreamStreamCounts(total_stream_count, stereo_stream_count));
shared_memory->dsp_return_data[0] = OpusMultiStreamDecodeObject::GetWorkBufferSize(
total_stream_count, stereo_stream_count);
Send(Direction::Host, Message::GetWorkBufferSizeForMultiStreamOK);
} break;
case InitializeMultiStreamDecodeObject: {
auto buffer = shared_memory->host_send_data[0];
auto buffer_size = shared_memory->host_send_data[1];
auto sample_rate = static_cast<s32>(shared_memory->host_send_data[2]);
auto channel_count = static_cast<s32>(shared_memory->host_send_data[3]);
auto total_stream_count = static_cast<s32>(shared_memory->host_send_data[4]);
auto stereo_stream_count = static_cast<s32>(shared_memory->host_send_data[5]);
// Nintendo seem to have a bug here, they try to use &host_send_data[6] for the channel
// mappings, but [6] is never set, and there is not enough room in the argument data for
// more than 40 channels, when 255 are possible.
// It also means the mapping values are undefined, though likely always 0,
// and the mappings given by the game are ignored. The mappings are copied to this
// dedicated buffer host side, so let's do as intended.
auto mappings = shared_memory->channel_mapping.data();
ASSERT(IsValidMultiStreamStreamCounts(total_stream_count, stereo_stream_count));
ASSERT(sample_rate >= 0);
ASSERT(buffer_size >= OpusMultiStreamDecodeObject::GetWorkBufferSize(
total_stream_count, stereo_stream_count));
auto& decoder_object = OpusMultiStreamDecodeObject::Initialize(buffer, buffer);
shared_memory->dsp_return_data[0] = decoder_object.InitializeDecoder(
sample_rate, total_stream_count, channel_count, stereo_stream_count, mappings);
Send(Direction::Host, Message::InitializeMultiStreamDecodeObjectOK);
} break;
case ShutdownMultiStreamDecodeObject: {
auto buffer = shared_memory->host_send_data[0];
[[maybe_unused]] auto buffer_size = shared_memory->host_send_data[1];
auto& decoder_object = OpusMultiStreamDecodeObject::Initialize(buffer, buffer);
shared_memory->dsp_return_data[0] = decoder_object.Shutdown();
Send(Direction::Host, Message::ShutdownMultiStreamDecodeObjectOK);
} break;
case DecodeInterleavedForMultiStream: {
auto start_time = system.CoreTiming().GetGlobalTimeUs();
auto buffer = shared_memory->host_send_data[0];
auto input_data = shared_memory->host_send_data[1];
auto input_data_size = shared_memory->host_send_data[2];
auto output_data = shared_memory->host_send_data[3];
auto output_data_size = shared_memory->host_send_data[4];
auto final_range = static_cast<u32>(shared_memory->host_send_data[5]);
auto reset_requested = shared_memory->host_send_data[6];
u32 decoded_samples{0};
auto& decoder_object = OpusMultiStreamDecodeObject::Initialize(buffer, buffer);
s32 error_code{OPUS_OK};
if (reset_requested) {
error_code = decoder_object.ResetDecoder();
}
if (error_code == OPUS_OK) {
error_code = decoder_object.Decode(decoded_samples, output_data, output_data_size,
input_data, input_data_size);
}
if (error_code == OPUS_OK) {
if (final_range && decoder_object.GetFinalRange() != final_range) {
error_code = OPUS_INVALID_PACKET;
}
}
auto end_time = system.CoreTiming().GetGlobalTimeUs();
shared_memory->dsp_return_data[0] = error_code;
shared_memory->dsp_return_data[1] = decoded_samples;
shared_memory->dsp_return_data[2] = (end_time - start_time).count();
Send(Direction::Host, Message::DecodeInterleavedForMultiStreamOK);
} break;
default:
LOG_ERROR(Service_Audio, "Invalid OpusDecoder command {}", msg);
continue;
}
}
}
} // namespace AudioCore::ADSP::OpusDecoder

View File

@@ -0,0 +1,92 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <memory>
#include <thread>
#include "audio_core/adsp/apps/opus/shared_memory.h"
#include "audio_core/adsp/mailbox.h"
#include "common/common_types.h"
namespace Core {
class System;
} // namespace Core
namespace AudioCore::ADSP::OpusDecoder {
enum Message : u32 {
Invalid = 0,
Start = 1,
Shutdown = 2,
StartOK = 11,
ShutdownOK = 12,
GetWorkBufferSize = 21,
InitializeDecodeObject = 22,
ShutdownDecodeObject = 23,
DecodeInterleaved = 24,
MapMemory = 25,
UnmapMemory = 26,
GetWorkBufferSizeForMultiStream = 27,
InitializeMultiStreamDecodeObject = 28,
ShutdownMultiStreamDecodeObject = 29,
DecodeInterleavedForMultiStream = 30,
GetWorkBufferSizeOK = 41,
InitializeDecodeObjectOK = 42,
ShutdownDecodeObjectOK = 43,
DecodeInterleavedOK = 44,
MapMemoryOK = 45,
UnmapMemoryOK = 46,
GetWorkBufferSizeForMultiStreamOK = 47,
InitializeMultiStreamDecodeObjectOK = 48,
ShutdownMultiStreamDecodeObjectOK = 49,
DecodeInterleavedForMultiStreamOK = 50,
};
/**
* The AudioRenderer application running on the ADSP.
*/
class OpusDecoder {
public:
explicit OpusDecoder(Core::System& system);
~OpusDecoder();
bool IsRunning() const noexcept {
return running;
}
void Send(Direction dir, u32 message);
u32 Receive(Direction dir, std::stop_token stop_token = {});
void SetSharedMemory(SharedMemory& shared_memory_) {
shared_memory = &shared_memory_;
}
private:
/**
* Initializing thread, launched at audio_core boot to avoid blocking the main emu boot thread.
*/
void Init(std::stop_token stop_token);
/**
* Main OpusDecoder thread, responsible for processing the incoming Opus packets.
*/
void Main(std::stop_token stop_token);
/// Core system
Core::System& system;
/// Mailbox to communicate messages with the host, drives the main thread
Mailbox mailbox;
/// Init thread
std::jthread init_thread{};
/// Main thread
std::jthread main_thread{};
/// The current state
bool running{};
/// Structure shared with the host, input data set by the host before sending a mailbox message,
/// and the responses are written back by the OpusDecoder.
SharedMemory* shared_memory{};
};
} // namespace AudioCore::ADSP::OpusDecoder

View File

@@ -0,0 +1,111 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/adsp/apps/opus/opus_multistream_decode_object.h"
#include "common/assert.h"
namespace AudioCore::ADSP::OpusDecoder {
namespace {
bool IsValidChannelCount(u32 channel_count) {
return channel_count == 1 || channel_count == 2;
}
bool IsValidStreamCounts(u32 total_stream_count, u32 stereo_stream_count) {
return total_stream_count > 0 && static_cast<s32>(stereo_stream_count) >= 0 &&
stereo_stream_count <= total_stream_count && IsValidChannelCount(total_stream_count);
}
} // namespace
u32 OpusMultiStreamDecodeObject::GetWorkBufferSize(u32 total_stream_count,
u32 stereo_stream_count) {
if (IsValidStreamCounts(total_stream_count, stereo_stream_count)) {
return static_cast<u32>(sizeof(OpusMultiStreamDecodeObject)) +
opus_multistream_decoder_get_size(total_stream_count, stereo_stream_count);
}
return 0;
}
OpusMultiStreamDecodeObject& OpusMultiStreamDecodeObject::Initialize(u64 buffer, u64 buffer2) {
auto* new_decoder = reinterpret_cast<OpusMultiStreamDecodeObject*>(buffer);
auto* comparison = reinterpret_cast<OpusMultiStreamDecodeObject*>(buffer2);
if (new_decoder->magic == DecodeMultiStreamObjectMagic) {
if (!new_decoder->initialized ||
(new_decoder->initialized && new_decoder->self == comparison)) {
new_decoder->state_valid = true;
}
} else {
new_decoder->initialized = false;
new_decoder->state_valid = true;
}
return *new_decoder;
}
s32 OpusMultiStreamDecodeObject::InitializeDecoder(u32 sample_rate, u32 total_stream_count,
u32 channel_count, u32 stereo_stream_count,
u8* mappings) {
if (!state_valid) {
return OPUS_INVALID_STATE;
}
if (initialized) {
return OPUS_OK;
}
// See OpusDecodeObject::InitializeDecoder for an explanation of this
decoder = (LibOpusMSDecoder*)(this + 1);
s32 ret = opus_multistream_decoder_init(decoder, sample_rate, channel_count, total_stream_count,
stereo_stream_count, mappings);
if (ret == OPUS_OK) {
magic = DecodeMultiStreamObjectMagic;
initialized = true;
state_valid = true;
self = this;
final_range = 0;
}
return ret;
}
s32 OpusMultiStreamDecodeObject::Shutdown() {
if (!state_valid) {
return OPUS_INVALID_STATE;
}
if (initialized) {
magic = 0x0;
initialized = false;
state_valid = false;
self = nullptr;
final_range = 0;
decoder = nullptr;
}
return OPUS_OK;
}
s32 OpusMultiStreamDecodeObject::ResetDecoder() {
return opus_multistream_decoder_ctl(decoder, OPUS_RESET_STATE);
}
s32 OpusMultiStreamDecodeObject::Decode(u32& out_sample_count, u64 output_data,
u64 output_data_size, u64 input_data, u64 input_data_size) {
ASSERT(initialized);
out_sample_count = 0;
if (!state_valid) {
return OPUS_INVALID_STATE;
}
auto ret_code_or_samples = opus_multistream_decode(
decoder, reinterpret_cast<const u8*>(input_data), static_cast<opus_int32>(input_data_size),
reinterpret_cast<opus_int16*>(output_data), static_cast<opus_int32>(output_data_size), 0);
if (ret_code_or_samples < OPUS_OK) {
return ret_code_or_samples;
}
out_sample_count = ret_code_or_samples;
return opus_multistream_decoder_ctl(decoder, OPUS_GET_FINAL_RANGE_REQUEST, &final_range);
}
} // namespace AudioCore::ADSP::OpusDecoder

View File

@@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <opus_multistream.h>
#include "common/common_types.h"
namespace AudioCore::ADSP::OpusDecoder {
using LibOpusMSDecoder = ::OpusMSDecoder;
static constexpr u32 DecodeMultiStreamObjectMagic = 0xDEADBEEF;
class OpusMultiStreamDecodeObject {
public:
static u32 GetWorkBufferSize(u32 total_stream_count, u32 stereo_stream_count);
static OpusMultiStreamDecodeObject& Initialize(u64 buffer, u64 buffer2);
s32 InitializeDecoder(u32 sample_rate, u32 total_stream_count, u32 channel_count,
u32 stereo_stream_count, u8* mappings);
s32 Shutdown();
s32 ResetDecoder();
s32 Decode(u32& out_sample_count, u64 output_data, u64 output_data_size, u64 input_data,
u64 input_data_size);
u32 GetFinalRange() const noexcept {
return final_range;
}
private:
u32 magic;
bool initialized;
bool state_valid;
OpusMultiStreamDecodeObject* self;
u32 final_range;
LibOpusMSDecoder* decoder;
};
static_assert(std::is_trivially_constructible_v<OpusMultiStreamDecodeObject>);
} // namespace AudioCore::ADSP::OpusDecoder

View File

@@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/common_funcs.h"
#include "common/common_types.h"
namespace AudioCore::ADSP::OpusDecoder {
struct SharedMemory {
std::array<u8, 0x100> channel_mapping{};
std::array<u64, 16> host_send_data{};
std::array<u64, 16> dsp_return_data{};
};
} // namespace AudioCore::ADSP::OpusDecoder

View File

60
src/audio_core/adsp/mailbox.h Executable file
View File

@@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <span>
#include "common/bounded_threadsafe_queue.h"
#include "common/common_types.h"
namespace AudioCore::ADSP {
enum class AppMailboxId : u32 {
Invalid = 0,
AudioRenderer = 50,
AudioRendererMemoryMapUnmap = 51,
};
enum class Direction : u32 {
Host,
DSP,
};
class Mailbox {
public:
void Initialize(AppMailboxId id_) {
Reset();
id = id_;
}
AppMailboxId Id() const noexcept {
return id;
}
void Send(Direction dir, u32 message) {
auto& queue = dir == Direction::Host ? host_queue : adsp_queue;
queue.EmplaceWait(message);
}
u32 Receive(Direction dir, std::stop_token stop_token = {}) {
auto& queue = dir == Direction::Host ? host_queue : adsp_queue;
return queue.PopWait(stop_token);
}
void Reset() {
id = AppMailboxId::Invalid;
u32 t{};
while (host_queue.TryPop(t)) {
}
while (adsp_queue.TryPop(t)) {
}
}
private:
AppMailboxId id{0};
Common::SPSCQueue<u32> host_queue;
Common::SPSCQueue<u32> adsp_queue;
};
} // namespace AudioCore::ADSP

50
src/audio_core/audio_core.cpp Executable file
View File

@@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/audio_core.h"
#include "audio_core/sink/sink_details.h"
#include "common/settings.h"
#include "core/core.h"
namespace AudioCore {
AudioCore::AudioCore(Core::System& system) : audio_manager{std::make_unique<AudioManager>()} {
CreateSinks();
// Must be created after the sinks
adsp = std::make_unique<ADSP::ADSP>(system, *output_sink);
}
AudioCore ::~AudioCore() {
Shutdown();
}
void AudioCore::CreateSinks() {
const auto& sink_id{Settings::values.sink_id};
const auto& audio_output_device_id{Settings::values.audio_output_device_id};
const auto& audio_input_device_id{Settings::values.audio_input_device_id};
output_sink = Sink::CreateSinkFromID(sink_id.GetValue(), audio_output_device_id.GetValue());
input_sink = Sink::CreateSinkFromID(sink_id.GetValue(), audio_input_device_id.GetValue());
}
void AudioCore::Shutdown() {
audio_manager->Shutdown();
}
AudioManager& AudioCore::GetAudioManager() {
return *audio_manager;
}
Sink::Sink& AudioCore::GetOutputSink() {
return *output_sink;
}
Sink::Sink& AudioCore::GetInputSink() {
return *input_sink;
}
ADSP::ADSP& AudioCore::ADSP() {
return *adsp;
}
} // namespace AudioCore

76
src/audio_core/audio_core.h Executable file
View File

@@ -0,0 +1,76 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <memory>
#include "audio_core/adsp/adsp.h"
#include "audio_core/audio_manager.h"
#include "audio_core/sink/sink.h"
namespace Core {
class System;
}
namespace AudioCore {
class AudioManager;
/**
* Main audio class, stored inside the core, and holding the audio manager, all sinks, and the ADSP.
*/
class AudioCore {
public:
explicit AudioCore(Core::System& system);
~AudioCore();
/**
* Shutdown the audio core.
*/
void Shutdown();
/**
* Get a reference to the audio manager.
*
* @return Ref to the audio manager.
*/
AudioManager& GetAudioManager();
/**
* Get the audio output sink currently in use.
*
* @return Ref to the sink.
*/
Sink::Sink& GetOutputSink();
/**
* Get the audio input sink currently in use.
*
* @return Ref to the sink.
*/
Sink::Sink& GetInputSink();
/**
* Get the ADSP.
*
* @return Ref to the ADSP.
*/
ADSP::ADSP& ADSP();
private:
/**
* Create the sinks on startup.
*/
void CreateSinks();
/// Main audio manager for audio in/out
std::unique_ptr<AudioManager> audio_manager;
/// Sink used for audio renderer and audio out
std::unique_ptr<Sink::Sink> output_sink;
/// Sink used for audio input
std::unique_ptr<Sink::Sink> input_sink;
/// The ADSP in the sysmodule
std::unique_ptr<ADSP::ADSP> adsp;
};
} // namespace AudioCore

61
src/audio_core/audio_event.cpp Executable file
View File

@@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/audio_event.h"
#include "common/assert.h"
#include "common/polyfill_ranges.h"
namespace AudioCore {
size_t Event::GetManagerIndex(const Type type) const {
switch (type) {
case Type::AudioInManager:
return 0;
case Type::AudioOutManager:
return 1;
case Type::FinalOutputRecorderManager:
return 2;
case Type::Max:
return 3;
default:
UNREACHABLE();
}
}
void Event::SetAudioEvent(const Type type, const bool signalled) {
events_signalled[GetManagerIndex(type)] = signalled;
if (signalled) {
manager_event.notify_one();
}
}
bool Event::CheckAudioEventSet(const Type type) const {
return events_signalled[GetManagerIndex(type)];
}
std::mutex& Event::GetAudioEventLock() {
return event_lock;
}
std::condition_variable_any& Event::GetAudioEvent() {
return manager_event;
}
bool Event::Wait(std::unique_lock<std::mutex>& l, const std::chrono::seconds timeout) {
bool timed_out{false};
if (!manager_event.wait_for(l, timeout, [&]() {
return std::ranges::any_of(events_signalled, [](bool x) { return x; });
})) {
timed_out = true;
}
return timed_out;
}
void Event::ClearEvents() {
events_signalled[0] = false;
events_signalled[1] = false;
events_signalled[2] = false;
events_signalled[3] = false;
}
} // namespace AudioCore

92
src/audio_core/audio_event.h Executable file
View File

@@ -0,0 +1,92 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <mutex>
namespace AudioCore {
/**
* Responsible for the input/output events, set by the stream backend when buffers are consumed, and
* waited on by the audio manager. These callbacks signal the game's events to keep the audio buffer
* recycling going.
* In a real Switch this is not a separate class, and exists entirely within the audio manager.
* On the Switch it's implemented more simply through a MultiWaitEventHolder, where it can
* wait on multiple events at once, and the events are not needed by the backend.
*/
class Event {
public:
enum class Type {
AudioInManager,
AudioOutManager,
FinalOutputRecorderManager,
Max,
};
/**
* Convert a manager type to an index.
*
* @param type - The manager type to convert
* @return The index of the type.
*/
size_t GetManagerIndex(Type type) const;
/**
* Set an audio event to true or false.
*
* @param type - The manager type to signal.
* @param signalled - Its signal state.
*/
void SetAudioEvent(Type type, bool signalled);
/**
* Check if the given manager type is signalled.
*
* @param type - The manager type to check.
* @return True if the event is signalled, otherwise false.
*/
bool CheckAudioEventSet(Type type) const;
/**
* Get the lock for audio events.
*
* @return Reference to the lock.
*/
std::mutex& GetAudioEventLock();
/**
* Get the manager event, this signals the audio manager to release buffers and signal the game
* for more.
*
* @return Reference to the condition variable.
*/
std::condition_variable_any& GetAudioEvent();
/**
* Wait on the manager_event.
*
* @param l - Lock held by the wait.
* @param timeout - Timeout for the wait. This is 2 seconds by default.
* @return True if the wait timed out, otherwise false if signalled.
*/
bool Wait(std::unique_lock<std::mutex>& l, std::chrono::seconds timeout);
/**
* Reset all manager events.
*/
void ClearEvents();
private:
/// Lock, used by the audio manager
std::mutex event_lock;
/// Array of events, one per system type (see Type), last event is used to terminate
std::array<std::atomic<bool>, 4> events_signalled;
/// Event to signal the audio manager
std::condition_variable_any manager_event;
};
} // namespace AudioCore

View File

@@ -0,0 +1,90 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/audio_core.h"
#include "audio_core/audio_in_manager.h"
#include "audio_core/audio_manager.h"
#include "audio_core/in/audio_in.h"
#include "audio_core/sink/sink_details.h"
#include "common/settings.h"
#include "core/core.h"
#include "core/hle/service/audio/errors.h"
namespace AudioCore::AudioIn {
Manager::Manager(Core::System& system_) : system{system_} {
std::iota(session_ids.begin(), session_ids.end(), 0);
num_free_sessions = MaxInSessions;
}
Result Manager::AcquireSessionId(size_t& session_id) {
if (num_free_sessions == 0) {
LOG_ERROR(Service_Audio, "All 4 AudioIn sessions are in use, cannot create any more");
return Service::Audio::ResultOutOfSessions;
}
session_id = session_ids[next_session_id];
next_session_id = (next_session_id + 1) % MaxInSessions;
num_free_sessions--;
return ResultSuccess;
}
void Manager::ReleaseSessionId(const size_t session_id) {
std::scoped_lock l{mutex};
LOG_DEBUG(Service_Audio, "Freeing AudioIn session {}", session_id);
session_ids[free_session_id] = session_id;
num_free_sessions++;
free_session_id = (free_session_id + 1) % MaxInSessions;
sessions[session_id].reset();
applet_resource_user_ids[session_id] = 0;
}
Result Manager::LinkToManager() {
std::scoped_lock l{mutex};
if (!linked_to_manager) {
AudioManager& manager{system.AudioCore().GetAudioManager()};
manager.SetInManager(std::bind(&Manager::BufferReleaseAndRegister, this));
linked_to_manager = true;
}
return ResultSuccess;
}
void Manager::Start() {
if (sessions_started) {
return;
}
std::scoped_lock l{mutex};
for (auto& session : sessions) {
if (session) {
session->StartSession();
}
}
sessions_started = true;
}
void Manager::BufferReleaseAndRegister() {
std::scoped_lock l{mutex};
for (auto& session : sessions) {
if (session != nullptr) {
session->ReleaseAndRegisterBuffers();
}
}
}
u32 Manager::GetDeviceNames(std::span<Renderer::AudioDevice::AudioDeviceName> names,
[[maybe_unused]] const bool filter) {
std::scoped_lock l{mutex};
LinkToManager();
auto input_devices{Sink::GetDeviceListForSink(Settings::values.sink_id.GetValue(), true)};
if (!input_devices.empty() && !names.empty()) {
names[0] = Renderer::AudioDevice::AudioDeviceName("Uac");
return 1;
}
return 0;
}
} // namespace AudioCore::AudioIn

View File

@@ -0,0 +1,91 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <mutex>
#include <vector>
#include "audio_core/renderer/audio_device.h"
namespace Core {
class System;
}
namespace AudioCore::AudioIn {
class In;
constexpr size_t MaxInSessions = 4;
/**
* Manages all audio in sessions.
*/
class Manager {
public:
explicit Manager(Core::System& system);
/**
* Acquire a free session id for opening a new audio in.
*
* @param session_id - Output session_id.
* @return Result code.
*/
Result AcquireSessionId(size_t& session_id);
/**
* Release a session id on close.
*
* @param session_id - Session id to free.
*/
void ReleaseSessionId(size_t session_id);
/**
* Link the audio in manager to the main audio manager.
*
* @return Result code.
*/
Result LinkToManager();
/**
* Start the audio in manager.
*/
void Start();
/**
* Callback function, called by the audio manager when the audio in event is signalled.
*/
void BufferReleaseAndRegister();
/**
* Get a list of audio in device names.
*
* @param names - Output container to write names to.
* @param filter - Should the list be filtered? Unused.
*
* @return Number of names written.
*/
u32 GetDeviceNames(std::span<Renderer::AudioDevice::AudioDeviceName> names, bool filter);
/// Core system
Core::System& system;
/// Array of session ids
std::array<size_t, MaxInSessions> session_ids{};
/// Array of resource user ids
std::array<size_t, MaxInSessions> applet_resource_user_ids{};
/// Pointer to each open session
std::array<std::shared_ptr<In>, MaxInSessions> sessions{};
/// The number of free sessions
size_t num_free_sessions{};
/// The next session id to be taken
size_t next_session_id{};
/// The next session id to be freed
size_t free_session_id{};
/// Whether this is linked to the audio manager
bool linked_to_manager{};
/// Whether the sessions have been started
bool sessions_started{};
/// Protect state due to audio manager callback
std::recursive_mutex mutex{};
};
} // namespace AudioCore::AudioIn

View File

@@ -0,0 +1,81 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/audio_manager.h"
#include "core/core.h"
#include "core/hle/service/audio/errors.h"
namespace AudioCore {
AudioManager::AudioManager() {
thread = std::jthread([this]() { ThreadFunc(); });
}
void AudioManager::Shutdown() {
running = false;
events.SetAudioEvent(Event::Type::Max, true);
thread.join();
}
Result AudioManager::SetOutManager(BufferEventFunc buffer_func) {
if (!running) {
return Service::Audio::ResultOperationFailed;
}
std::scoped_lock l{lock};
const auto index{events.GetManagerIndex(Event::Type::AudioOutManager)};
if (buffer_events[index] == nullptr) {
buffer_events[index] = std::move(buffer_func);
needs_update = true;
events.SetAudioEvent(Event::Type::AudioOutManager, true);
}
return ResultSuccess;
}
Result AudioManager::SetInManager(BufferEventFunc buffer_func) {
if (!running) {
return Service::Audio::ResultOperationFailed;
}
std::scoped_lock l{lock};
const auto index{events.GetManagerIndex(Event::Type::AudioInManager)};
if (buffer_events[index] == nullptr) {
buffer_events[index] = std::move(buffer_func);
needs_update = true;
events.SetAudioEvent(Event::Type::AudioInManager, true);
}
return ResultSuccess;
}
void AudioManager::SetEvent(const Event::Type type, const bool signalled) {
events.SetAudioEvent(type, signalled);
}
void AudioManager::ThreadFunc() {
std::unique_lock l{events.GetAudioEventLock()};
events.ClearEvents();
running = true;
while (running) {
const auto timed_out{events.Wait(l, std::chrono::seconds(2))};
if (events.CheckAudioEventSet(Event::Type::Max)) {
break;
}
for (size_t i = 0; i < buffer_events.size(); i++) {
const auto event_type = static_cast<Event::Type>(i);
if (events.CheckAudioEventSet(event_type) || timed_out) {
if (buffer_events[i]) {
buffer_events[i]();
}
}
events.SetAudioEvent(event_type, false);
}
}
}
} // namespace AudioCore

88
src/audio_core/audio_manager.h Executable file
View File

@@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <atomic>
#include <functional>
#include <mutex>
#include <thread>
#include "common/polyfill_thread.h"
#include "audio_core/audio_event.h"
union Result;
namespace AudioCore {
/**
* The AudioManager's main purpose is to wait for buffer events for the audio in and out managers,
* and call an associated callback to release buffers.
*
* Execution pattern is:
* Buffers appended ->
* Buffers queued and played by the backend stream ->
* When consumed, set the corresponding manager event and signal the audio manager ->
* Consumed buffers are released, game is signalled ->
* Game appends more buffers.
*
* This is only used by audio in and audio out.
*/
class AudioManager {
using BufferEventFunc = std::function<void()>;
public:
explicit AudioManager();
/**
* Shutdown the audio manager.
*/
void Shutdown();
/**
* Register the out manager, keeping a function to be called when the out event is signalled.
*
* @param buffer_func - Function to be called on signal.
* @return Result code.
*/
Result SetOutManager(BufferEventFunc buffer_func);
/**
* Register the in manager, keeping a function to be called when the in event is signalled.
*
* @param buffer_func - Function to be called on signal.
* @return Result code.
*/
Result SetInManager(BufferEventFunc buffer_func);
/**
* Set an event to signalled, and signal the thread.
*
* @param type - Manager type to set.
* @param signalled - Set the event to true or false?
*/
void SetEvent(Event::Type type, bool signalled);
private:
/**
* Main thread, waiting on a manager signal and calling the registered function.
*/
void ThreadFunc();
/// Is the main thread running?
std::atomic<bool> running{};
/// Unused
bool needs_update{};
/// Events to be set and signalled
Event events{};
/// Callbacks for each manager
std::array<BufferEventFunc, 3> buffer_events{};
/// General lock
std::mutex lock{};
/// Main thread for waiting and callbacks
std::jthread thread;
};
} // namespace AudioCore

View File

@@ -0,0 +1,81 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/audio_core.h"
#include "audio_core/audio_manager.h"
#include "audio_core/audio_out_manager.h"
#include "audio_core/out/audio_out.h"
#include "core/core.h"
#include "core/hle/kernel/k_event.h"
#include "core/hle/service/audio/errors.h"
namespace AudioCore::AudioOut {
Manager::Manager(Core::System& system_) : system{system_} {
std::iota(session_ids.begin(), session_ids.end(), 0);
num_free_sessions = MaxOutSessions;
}
Result Manager::AcquireSessionId(size_t& session_id) {
if (num_free_sessions == 0) {
LOG_ERROR(Service_Audio, "All 12 Audio Out sessions are in use, cannot create any more");
return Service::Audio::ResultOutOfSessions;
}
session_id = session_ids[next_session_id];
next_session_id = (next_session_id + 1) % MaxOutSessions;
num_free_sessions--;
return ResultSuccess;
}
void Manager::ReleaseSessionId(const size_t session_id) {
std::scoped_lock l{mutex};
LOG_DEBUG(Service_Audio, "Freeing AudioOut session {}", session_id);
session_ids[free_session_id] = session_id;
num_free_sessions++;
free_session_id = (free_session_id + 1) % MaxOutSessions;
sessions[session_id].reset();
applet_resource_user_ids[session_id] = 0;
}
Result Manager::LinkToManager() {
std::scoped_lock l{mutex};
if (!linked_to_manager) {
AudioManager& manager{system.AudioCore().GetAudioManager()};
manager.SetOutManager(std::bind(&Manager::BufferReleaseAndRegister, this));
linked_to_manager = true;
}
return ResultSuccess;
}
void Manager::Start() {
if (sessions_started) {
return;
}
std::scoped_lock l{mutex};
for (auto& session : sessions) {
if (session) {
session->StartSession();
}
}
sessions_started = true;
}
void Manager::BufferReleaseAndRegister() {
std::scoped_lock l{mutex};
for (auto& session : sessions) {
if (session != nullptr) {
session->ReleaseAndRegisterBuffers();
}
}
}
u32 Manager::GetAudioOutDeviceNames(
std::vector<Renderer::AudioDevice::AudioDeviceName>& names) const {
names.emplace_back("DeviceOut");
return 1;
}
} // namespace AudioCore::AudioOut

View File

@@ -0,0 +1,88 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <mutex>
#include "audio_core/renderer/audio_device.h"
namespace Core {
class System;
}
namespace AudioCore::AudioOut {
class Out;
constexpr size_t MaxOutSessions = 12;
/**
* Manages all audio out sessions.
*/
class Manager {
public:
explicit Manager(Core::System& system);
/**
* Acquire a free session id for opening a new audio out.
*
* @param session_id - Output session_id.
* @return Result code.
*/
Result AcquireSessionId(size_t& session_id);
/**
* Release a session id on close.
*
* @param session_id - Session id to free.
*/
void ReleaseSessionId(size_t session_id);
/**
* Link this manager to the main audio manager.
*
* @return Result code.
*/
Result LinkToManager();
/**
* Start the audio out manager.
*/
void Start();
/**
* Callback function, called by the audio manager when the audio out event is signalled.
*/
void BufferReleaseAndRegister();
/**
* Get a list of audio out device names.
*
* @param names - Output container to write names to.
* @return Number of names written.
*/
u32 GetAudioOutDeviceNames(std::vector<Renderer::AudioDevice::AudioDeviceName>& names) const;
/// Core system
Core::System& system;
/// Array of session ids
std::array<size_t, MaxOutSessions> session_ids{};
/// Array of resource user ids
std::array<size_t, MaxOutSessions> applet_resource_user_ids{};
/// Pointer to each open session
std::array<std::shared_ptr<Out>, MaxOutSessions> sessions{};
/// The number of free sessions
size_t num_free_sessions{};
/// The next session id to be taken
size_t next_session_id{};
/// The next session id to be freed
size_t free_session_id{};
/// Whether this is linked to the audio manager
bool linked_to_manager{};
/// Whether the sessions have been started
bool sessions_started{};
/// Protect state due to audio manager callback
std::recursive_mutex mutex{};
};
} // namespace AudioCore::AudioOut

View File

@@ -0,0 +1,70 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/audio_render_manager.h"
#include "audio_core/common/audio_renderer_parameter.h"
#include "audio_core/common/feature_support.h"
#include "core/core.h"
namespace AudioCore::Renderer {
Manager::Manager(Core::System& system_)
: system{system_}, system_manager{std::make_unique<SystemManager>(system)} {
std::iota(session_ids.begin(), session_ids.end(), 0);
}
Manager::~Manager() {
Stop();
}
void Manager::Stop() {
system_manager->Stop();
}
SystemManager& Manager::GetSystemManager() {
return *system_manager;
}
Result Manager::GetWorkBufferSize(const AudioRendererParameterInternal& params,
u64& out_count) const {
if (!CheckValidRevision(params.revision)) {
return Service::Audio::ResultInvalidRevision;
}
out_count = System::GetWorkBufferSize(params);
return ResultSuccess;
}
s32 Manager::GetSessionId() {
std::scoped_lock l{session_lock};
auto session_id{session_ids[session_count]};
if (session_id == -1) {
return -1;
}
session_ids[session_count] = -1;
session_count++;
return session_id;
}
void Manager::ReleaseSessionId(const s32 session_id) {
std::scoped_lock l{session_lock};
session_ids[--session_count] = session_id;
}
u32 Manager::GetSessionCount() const {
std::scoped_lock l{session_lock};
return session_count;
}
bool Manager::AddSystem(System& system_) {
return system_manager->Add(system_);
}
bool Manager::RemoveSystem(System& system_) {
return system_manager->Remove(system_);
}
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,105 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <memory>
#include <mutex>
#include "common/polyfill_thread.h"
#include "audio_core/common/common.h"
#include "audio_core/renderer/system_manager.h"
#include "core/hle/service/audio/errors.h"
namespace Core {
class System;
}
namespace AudioCore {
struct AudioRendererParameterInternal;
namespace Renderer {
/**
* Wrapper for the audio system manager, handles service calls.
*/
class Manager {
public:
explicit Manager(Core::System& system);
~Manager();
/**
* Stop the manager.
*/
void Stop();
/**
* Get the system manager.
*
* @return The system manager.
*/
SystemManager& GetSystemManager();
/**
* Get required size for the audio renderer workbuffer.
*
* @param params - Input parameters with the numbers of voices/mixes/sinks etc.
* @param out_count - Output size of the required workbuffer.
* @return Result code.
*/
Result GetWorkBufferSize(const AudioRendererParameterInternal& params, u64& out_count) const;
/**
* Get a new session id.
*
* @return The new session id. -1 if invalid, otherwise 0-MaxRendererSessions.
*/
s32 GetSessionId();
/**
* Get the number of currently active sessions.
*
* @return The number of active sessions.
*/
u32 GetSessionCount() const;
/**
* Add a renderer system to the manager.
* The system will be regularly called to generate commands for the AudioRenderer.
*
* @param system - The system to add.
* @return True if the system was successfully added, otherwise false.
*/
bool AddSystem(System& system);
/**
* Remove a renderer system from the manager.
*
* @param system - The system to remove.
* @return True if the system was successfully removed, otherwise false.
*/
bool RemoveSystem(System& system);
/**
* Free a session id when the system wants to shut down.
*
* @param session_id - The session id to free.
*/
void ReleaseSessionId(s32 session_id);
private:
/// Core system
Core::System& system;
/// Session ids, -1 when in use
std::array<s32, MaxRendererSessions> session_ids{};
/// Number of active renderers
u32 session_count{};
/// Lock for interacting with the sessions
mutable std::mutex session_lock{};
/// Regularly generates commands from the registered systems for the AudioRenderer
std::unique_ptr<SystemManager> system_manager{};
};
} // namespace Renderer
} // namespace AudioCore

View File

@@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <span>
#include "audio_core/renderer/behavior/behavior_info.h"
#include "audio_core/renderer/memory/memory_pool_info.h"
#include "audio_core/renderer/upsampler/upsampler_manager.h"
#include "common/common_types.h"
namespace AudioCore {
/**
* Execution mode of the audio renderer.
* Only Auto is currently supported.
*/
enum class ExecutionMode : u8 {
Auto,
Manual,
};
/**
* Parameters from the game, passed to the audio renderer for initialisation.
*/
struct AudioRendererParameterInternal {
/* 0x00 */ u32 sample_rate;
/* 0x04 */ u32 sample_count;
/* 0x08 */ u32 mixes;
/* 0x0C */ u32 sub_mixes;
/* 0x10 */ u32 voices;
/* 0x14 */ u32 sinks;
/* 0x18 */ u32 effects;
/* 0x1C */ u32 perf_frames;
/* 0x20 */ u8 voice_drop_enabled;
/* 0x21 */ u8 unk_21;
/* 0x22 */ u8 rendering_device;
/* 0x23 */ ExecutionMode execution_mode;
/* 0x24 */ u32 splitter_infos;
/* 0x28 */ s32 splitter_destinations;
/* 0x2C */ u32 external_context_size;
/* 0x30 */ u32 revision;
};
static_assert(sizeof(AudioRendererParameterInternal) == 0x34,
"AudioRendererParameterInternal has the wrong size!");
/**
* Context for rendering, contains a bunch of useful fields for the command generator.
*/
struct AudioRendererSystemContext {
s32 session_id;
s8 channels;
s16 mix_buffer_count;
Renderer::BehaviorInfo* behavior;
std::span<s32> depop_buffer;
Renderer::UpsamplerManager* upsampler_manager;
Renderer::MemoryPoolInfo* memory_pool_info;
};
} // namespace AudioCore

138
src/audio_core/common/common.h Executable file
View File

@@ -0,0 +1,138 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <numeric>
#include <span>
#include "common/assert.h"
#include "common/common_funcs.h"
#include "common/common_types.h"
namespace AudioCore {
using CpuAddr = std::uintptr_t;
enum class PlayState : u8 {
Started,
Stopped,
Paused,
};
enum class SrcQuality : u8 {
Medium,
High,
Low,
};
enum class SampleFormat : u8 {
Invalid,
PcmInt8,
PcmInt16,
PcmInt24,
PcmInt32,
PcmFloat,
Adpcm,
};
enum class SessionTypes {
AudioIn,
AudioOut,
FinalOutputRecorder,
};
enum class Channels : u32 {
FrontLeft,
FrontRight,
Center,
LFE,
BackLeft,
BackRight,
};
// These are used by Delay, Reverb and I3dl2Reverb prior to Revision 11.
enum class OldChannels : u32 {
FrontLeft,
FrontRight,
BackLeft,
BackRight,
Center,
LFE,
};
constexpr u32 BufferCount = 32;
constexpr u32 MaxRendererSessions = 2;
constexpr u32 TargetSampleCount = 240;
constexpr u32 TargetSampleRate = 48'000;
constexpr u32 MaxChannels = 6;
constexpr u32 MaxMixBuffers = 24;
constexpr u32 MaxWaveBuffers = 4;
constexpr s32 LowestVoicePriority = 0xFF;
constexpr s32 HighestVoicePriority = 0;
constexpr u32 BufferAlignment = 0x40;
constexpr u32 WorkbufferAlignment = 0x1000;
constexpr s32 FinalMixId = 0;
constexpr s32 InvalidDistanceFromFinalMix = std::numeric_limits<s32>::min();
constexpr s32 UnusedSplitterId = -1;
constexpr s32 UnusedMixId = std::numeric_limits<s32>::max();
constexpr u32 InvalidNodeId = 0xF0000000;
constexpr s32 InvalidProcessOrder = -1;
constexpr u32 MaxBiquadFilters = 2;
constexpr u32 MaxEffects = 256;
constexpr bool IsChannelCountValid(u16 channel_count) {
return channel_count <= 6 &&
(channel_count == 1 || channel_count == 2 || channel_count == 4 || channel_count == 6);
}
constexpr void UseOldChannelMapping(std::span<s16> inputs, std::span<s16> outputs) {
constexpr auto old_center{static_cast<u32>(OldChannels::Center)};
constexpr auto new_center{static_cast<u32>(Channels::Center)};
constexpr auto old_lfe{static_cast<u32>(OldChannels::LFE)};
constexpr auto new_lfe{static_cast<u32>(Channels::LFE)};
auto center{inputs[old_center]};
auto lfe{inputs[old_lfe]};
inputs[old_center] = inputs[new_center];
inputs[old_lfe] = inputs[new_lfe];
inputs[new_center] = center;
inputs[new_lfe] = lfe;
center = outputs[old_center];
lfe = outputs[old_lfe];
outputs[old_center] = outputs[new_center];
outputs[old_lfe] = outputs[new_lfe];
outputs[new_center] = center;
outputs[new_lfe] = lfe;
}
constexpr u32 GetSplitterInParamHeaderMagic() {
return Common::MakeMagic('S', 'N', 'D', 'H');
}
constexpr u32 GetSplitterInfoMagic() {
return Common::MakeMagic('S', 'N', 'D', 'I');
}
constexpr u32 GetSplitterSendDataMagic() {
return Common::MakeMagic('S', 'N', 'D', 'D');
}
constexpr size_t GetSampleFormatByteSize(SampleFormat format) {
switch (format) {
case SampleFormat::PcmInt8:
return 1;
case SampleFormat::PcmInt16:
return 2;
case SampleFormat::PcmInt24:
return 3;
case SampleFormat::PcmInt32:
case SampleFormat::PcmFloat:
return 4;
default:
return 2;
}
}
} // namespace AudioCore

View File

@@ -0,0 +1,106 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <map>
#include <ranges>
#include <tuple>
#include "common/assert.h"
#include "common/common_funcs.h"
#include "common/common_types.h"
#include "common/polyfill_ranges.h"
namespace AudioCore {
constexpr u32 CurrentRevision = 11;
enum class SupportTags {
CommandProcessingTimeEstimatorVersion4,
CommandProcessingTimeEstimatorVersion3,
CommandProcessingTimeEstimatorVersion2,
MultiTapBiquadFilterProcessing,
EffectInfoVer2,
WaveBufferVer2,
BiquadFilterFloatProcessing,
VolumeMixParameterPrecisionQ23,
MixInParameterDirtyOnlyUpdate,
BiquadFilterEffectStateClearBugFix,
VoicePlayedSampleCountResetAtLoopPoint,
VoicePitchAndSrcSkipped,
SplitterBugFix,
FlushVoiceWaveBuffers,
ElapsedFrameCount,
AudioRendererVariadicCommandBufferSize,
PerformanceMetricsDataFormatVersion2,
AudioRendererProcessingTimeLimit80Percent,
AudioRendererProcessingTimeLimit75Percent,
AudioRendererProcessingTimeLimit70Percent,
AdpcmLoopContextBugFix,
Splitter,
LongSizePreDelay,
AudioUsbDeviceOutput,
DeviceApiVersion2,
DelayChannelMappingChange,
ReverbChannelMappingChange,
I3dl2ReverbChannelMappingChange,
// Not a real tag, just here to get the count.
Size
};
constexpr u32 GetRevisionNum(u32 user_revision) {
if (user_revision >= 0x100) {
user_revision -= Common::MakeMagic('R', 'E', 'V', '0');
user_revision >>= 24;
}
return user_revision;
};
constexpr bool CheckFeatureSupported(SupportTags tag, u32 user_revision) {
constexpr std::array<std::pair<SupportTags, u32>, static_cast<u32>(SupportTags::Size)> features{
{
{SupportTags::AudioRendererProcessingTimeLimit70Percent, 1},
{SupportTags::Splitter, 2},
{SupportTags::AdpcmLoopContextBugFix, 2},
{SupportTags::LongSizePreDelay, 3},
{SupportTags::AudioUsbDeviceOutput, 4},
{SupportTags::AudioRendererProcessingTimeLimit75Percent, 4},
{SupportTags::VoicePlayedSampleCountResetAtLoopPoint, 5},
{SupportTags::VoicePitchAndSrcSkipped, 5},
{SupportTags::SplitterBugFix, 5},
{SupportTags::FlushVoiceWaveBuffers, 5},
{SupportTags::ElapsedFrameCount, 5},
{SupportTags::AudioRendererProcessingTimeLimit80Percent, 5},
{SupportTags::AudioRendererVariadicCommandBufferSize, 5},
{SupportTags::PerformanceMetricsDataFormatVersion2, 5},
{SupportTags::CommandProcessingTimeEstimatorVersion2, 5},
{SupportTags::BiquadFilterEffectStateClearBugFix, 6},
{SupportTags::BiquadFilterFloatProcessing, 7},
{SupportTags::VolumeMixParameterPrecisionQ23, 7},
{SupportTags::MixInParameterDirtyOnlyUpdate, 7},
{SupportTags::WaveBufferVer2, 8},
{SupportTags::CommandProcessingTimeEstimatorVersion3, 8},
{SupportTags::EffectInfoVer2, 9},
{SupportTags::CommandProcessingTimeEstimatorVersion4, 10},
{SupportTags::MultiTapBiquadFilterProcessing, 10},
{SupportTags::DelayChannelMappingChange, 11},
{SupportTags::ReverbChannelMappingChange, 11},
{SupportTags::I3dl2ReverbChannelMappingChange, 11},
}};
const auto& feature =
std::ranges::find_if(features, [tag](const auto& entry) { return entry.first == tag; });
if (feature == features.cend()) {
LOG_ERROR(Service_Audio, "Invalid SupportTag {}!", static_cast<u32>(tag));
return false;
}
user_revision = GetRevisionNum(user_revision);
return (*feature).second <= user_revision;
}
constexpr bool CheckValidRevision(u32 user_revision) {
return GetRevisionNum(user_revision) <= CurrentRevision;
};
} // namespace AudioCore

View File

@@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/common_types.h"
namespace AudioCore {
struct WaveBufferVersion1 {
CpuAddr buffer;
u64 buffer_size;
u32 start_offset;
u32 end_offset;
bool loop;
bool stream_ended;
CpuAddr context;
u64 context_size;
};
struct WaveBufferVersion2 {
CpuAddr buffer;
CpuAddr context;
u64 buffer_size;
u64 context_size;
u32 start_offset;
u32 end_offset;
u32 loop_start_offset;
u32 loop_end_offset;
s32 loop_count;
bool loop;
bool stream_ended;
};
} // namespace AudioCore

View File

@@ -0,0 +1,100 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <span>
#include "common/alignment.h"
#include "common/assert.h"
#include "common/common_types.h"
namespace AudioCore {
/**
* Responsible for allocating up a workbuffer into multiple pieces.
* Takes in a buffer and size (it does not own them), and allocates up the buffer via Allocate.
*/
class WorkbufferAllocator {
public:
explicit WorkbufferAllocator(std::span<u8> buffer_, u64 size_)
: buffer{reinterpret_cast<u64>(buffer_.data())}, size{size_} {}
/**
* Allocate the given count of T elements, aligned to alignment.
*
* @param count - The number of elements to allocate.
* @param alignment - The required starting alignment.
* @return Non-owning container of allocated elements.
*/
template <typename T>
std::span<T> Allocate(u64 count, u64 alignment) {
u64 out{0};
u64 byte_size{count * sizeof(T)};
if (byte_size > 0) {
auto current{buffer + offset};
auto aligned_buffer{Common::AlignUp(current, alignment)};
if (aligned_buffer + byte_size <= buffer + size) {
out = aligned_buffer;
offset = byte_size - buffer + aligned_buffer;
} else {
LOG_ERROR(
Service_Audio,
"Allocated buffer was too small to hold new alloc.\nAllocator size={:08X}, "
"offset={:08X}.\nAttempting to allocate {:08X} with alignment={:02X}",
size, offset, byte_size, alignment);
count = 0;
}
}
return std::span<T>(reinterpret_cast<T*>(out), count);
}
/**
* Align the current offset to the given alignment.
*
* @param alignment - The required starting alignment.
*/
void Align(u64 alignment) {
auto current{buffer + offset};
auto aligned_buffer{Common::AlignUp(current, alignment)};
offset = 0 - buffer + aligned_buffer;
}
/**
* Get the current buffer offset.
*
* @return The current allocating offset.
*/
u64 GetCurrentOffset() const {
return offset;
}
/**
* Get the current buffer size.
*
* @return The size of the current buffer.
*/
u64 GetSize() const {
return size;
}
/**
* Get the remaining size that can be allocated.
*
* @return The remaining size left in the buffer.
*/
u64 GetRemainingSize() const {
return size - offset;
}
private:
/// The buffer into which we are allocating.
u64 buffer;
/// Size of the buffer we're allocating to.
u64 size;
/// Current offset into the buffer, an error will be thrown if it exceeds size.
u64 offset{};
};
} // namespace AudioCore

View File

@@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/common_types.h"
namespace AudioCore {
struct AudioBuffer {
/// Timestamp this buffer started playing.
u64 start_timestamp;
/// Timestamp this buffer should finish playing.
u64 end_timestamp;
/// Timestamp this buffer completed playing.
s64 played_timestamp;
/// Game memory address for these samples.
VAddr samples;
/// Unique identifier for this buffer.
u64 tag;
/// Size of the samples buffer.
u64 size;
};
} // namespace AudioCore

View File

@@ -0,0 +1,325 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <mutex>
#include <span>
#include <vector>
#include <boost/container/static_vector.hpp>
#include "audio_buffer.h"
#include "audio_core/device/device_session.h"
#include "core/core_timing.h"
namespace AudioCore {
constexpr s32 BufferAppendLimit = 4;
/**
* A ringbuffer of N audio buffers.
* The buffer contains 3 sections:
* Appended - Buffers added to the ring, but have yet to be sent to the audio backend.
* Registered - Buffers sent to the backend and queued for playback.
* Released - Buffers which have been played, and can now be recycled.
* Any others are free/untracked.
*
* @tparam N - Maximum number of buffers in the ring.
*/
template <size_t N>
class AudioBuffers {
public:
explicit AudioBuffers(size_t limit) : append_limit{static_cast<u32>(limit)} {}
/**
* Append a new audio buffer to the ring.
*
* @param buffer - The new buffer.
*/
void AppendBuffer(const AudioBuffer& buffer) {
std::scoped_lock l{lock};
buffers[appended_index] = buffer;
appended_count++;
appended_index = (appended_index + 1) % append_limit;
}
/**
* Register waiting buffers, up to a maximum of BufferAppendLimit.
*
* @param out_buffers - The buffers which were registered.
*/
void RegisterBuffers(boost::container::static_vector<AudioBuffer, N>& out_buffers) {
std::scoped_lock l{lock};
const s32 to_register{std::min(std::min(appended_count, BufferAppendLimit),
BufferAppendLimit - registered_count)};
for (s32 i = 0; i < to_register; i++) {
s32 index{appended_index - appended_count};
if (index < 0) {
index += N;
}
out_buffers.push_back(buffers[index]);
registered_count++;
registered_index = (registered_index + 1) % append_limit;
appended_count--;
if (appended_count == 0) {
break;
}
}
}
/**
* Release a single buffer. Must be already registered.
*
* @param index - The buffer index to release.
* @param timestamp - The released timestamp for this buffer.
*/
void ReleaseBuffer(s32 index, s64 timestamp) {
std::scoped_lock l{lock};
buffers[index].played_timestamp = timestamp;
registered_count--;
released_count++;
released_index = (released_index + 1) % append_limit;
}
/**
* Release all registered buffers.
*
* @param core_timing - The CoreTiming instance
* @param session - The device session
*
* @return If any buffer was released.
*/
bool ReleaseBuffers(const Core::Timing::CoreTiming& core_timing, const DeviceSession& session,
bool force) {
std::scoped_lock l{lock};
bool buffer_released{false};
while (registered_count > 0) {
auto index{registered_index - registered_count};
if (index < 0) {
index += N;
}
// Check with the backend if this buffer can be released yet.
// If we're shutting down, we don't care if it's been played or not.
if (!force && !session.IsBufferConsumed(buffers[index])) {
break;
}
ReleaseBuffer(index, core_timing.GetGlobalTimeNs().count());
buffer_released = true;
}
return buffer_released || registered_count == 0;
}
/**
* Get all released buffers.
*
* @param tags - Container to be filled with the released buffers' tags.
* @return The number of buffers released.
*/
u32 GetReleasedBuffers(std::span<u64> tags) {
std::scoped_lock l{lock};
u32 released{0};
while (released_count > 0) {
auto index{released_index - released_count};
if (index < 0) {
index += N;
}
auto& buffer{buffers[index]};
released_count--;
auto tag{buffer.tag};
buffer.played_timestamp = 0;
buffer.samples = 0;
buffer.tag = 0;
buffer.size = 0;
if (tag == 0) {
break;
}
if (released < tags.size()) {
tags[released] = tag;
}
released++;
if (released >= tags.size()) {
break;
}
}
return released;
}
/**
* Get all appended and registered buffers.
*
* @param buffers_flushed - Output vector for the buffers which are released.
* @param max_buffers - Maximum number of buffers to released.
* @return The number of buffers released.
*/
u32 GetRegisteredAppendedBuffers(
boost::container::static_vector<AudioBuffer, N>& buffers_flushed, u32 max_buffers) {
std::scoped_lock l{lock};
if (registered_count + appended_count == 0) {
return 0;
}
size_t buffers_to_flush{
std::min(static_cast<u32>(registered_count + appended_count), max_buffers)};
if (buffers_to_flush == 0) {
return 0;
}
while (registered_count > 0) {
auto index{registered_index - registered_count};
if (index < 0) {
index += N;
}
buffers_flushed.push_back(buffers[index]);
registered_count--;
released_count++;
released_index = (released_index + 1) % append_limit;
if (buffers_flushed.size() >= buffers_to_flush) {
break;
}
}
while (appended_count > 0) {
auto index{appended_index - appended_count};
if (index < 0) {
index += N;
}
buffers_flushed.push_back(buffers[index]);
appended_count--;
released_count++;
released_index = (released_index + 1) % append_limit;
if (buffers_flushed.size() >= buffers_to_flush) {
break;
}
}
return static_cast<u32>(buffers_flushed.size());
}
/**
* Check if the given tag is in the buffers.
*
* @param tag - Unique tag of the buffer to search for.
* @return True if the buffer is still in the ring, otherwise false.
*/
bool ContainsBuffer(const u64 tag) const {
std::scoped_lock l{lock};
const auto registered_buffers{appended_count + registered_count + released_count};
if (registered_buffers == 0) {
return false;
}
auto index{released_index - released_count};
if (index < 0) {
index += append_limit;
}
for (s32 i = 0; i < registered_buffers; i++) {
if (buffers[index].tag == tag) {
return true;
}
index = (index + 1) % append_limit;
}
return false;
}
/**
* Get the number of active buffers in the ring.
* That is, appended, registered and released buffers.
*
* @return Number of active buffers.
*/
u32 GetAppendedRegisteredCount() const {
std::scoped_lock l{lock};
return appended_count + registered_count;
}
/**
* Get the total number of active buffers in the ring.
* That is, appended, registered and released buffers.
*
* @return Number of active buffers.
*/
u32 GetTotalBufferCount() const {
std::scoped_lock l{lock};
return static_cast<u32>(appended_count + registered_count + released_count);
}
/**
* Flush all of the currently appended and registered buffers
*
* @param buffers_released - Output count for the number of buffers released.
* @return True if buffers were successfully flushed, otherwise false.
*/
bool FlushBuffers(u32& buffers_released) {
std::scoped_lock l{lock};
boost::container::static_vector<AudioBuffer, N> buffers_flushed{};
buffers_released = GetRegisteredAppendedBuffers(buffers_flushed, append_limit);
if (registered_count > 0) {
return false;
}
if (static_cast<u32>(released_count + appended_count) > append_limit) {
return false;
}
return true;
}
u64 GetNextTimestamp() const {
// Iterate backwards through the buffer queue, and take the most recent buffer's end
std::scoped_lock l{lock};
auto index{appended_index - 1};
if (index < 0) {
index += append_limit;
}
return buffers[index].end_timestamp;
}
private:
/// Buffer lock
mutable std::recursive_mutex lock{};
/// The audio buffers
std::array<AudioBuffer, N> buffers{};
/// Current released index
s32 released_index{};
/// Number of released buffers
s32 released_count{};
/// Current registered index
s32 registered_index{};
/// Number of registered buffers
s32 registered_count{};
/// Current appended index
s32 appended_index{};
/// Number of appended buffers
s32 appended_count{};
/// Maximum number of buffers (default 32)
u32 append_limit{};
};
} // namespace AudioCore

View File

@@ -0,0 +1,144 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/audio_core.h"
#include "audio_core/audio_manager.h"
#include "audio_core/device/audio_buffer.h"
#include "audio_core/device/device_session.h"
#include "audio_core/sink/sink_stream.h"
#include "core/core.h"
#include "core/core_timing.h"
#include "core/guest_memory.h"
#include "core/memory.h"
#include "core/hle/kernel/k_process.h"
namespace AudioCore {
using namespace std::literals;
constexpr auto INCREMENT_TIME{5ms};
DeviceSession::DeviceSession(Core::System& system_)
: system{system_}, thread_event{Core::Timing::CreateEvent(
"AudioOutSampleTick",
[this](s64 time, std::chrono::nanoseconds) { return ThreadFunc(); })} {}
DeviceSession::~DeviceSession() {
Finalize();
}
Result DeviceSession::Initialize(std::string_view name_, SampleFormat sample_format_,
u16 channel_count_, size_t session_id_, Kernel::KProcess* handle_,
u64 applet_resource_user_id_, Sink::StreamType type_) {
if (stream) {
Finalize();
}
name = fmt::format("{}-{}", name_, session_id_);
type = type_;
sample_format = sample_format_;
channel_count = channel_count_;
session_id = session_id_;
handle = handle_;
handle->Open();
applet_resource_user_id = applet_resource_user_id_;
if (type == Sink::StreamType::In) {
sink = &system.AudioCore().GetInputSink();
} else {
sink = &system.AudioCore().GetOutputSink();
}
stream = sink->AcquireSinkStream(system, channel_count, name, type);
initialized = true;
return ResultSuccess;
}
void DeviceSession::Finalize() {
if (initialized) {
Stop();
sink->CloseStream(stream);
stream = nullptr;
}
if (handle) {
handle->Close();
handle = nullptr;
}
}
void DeviceSession::Start() {
if (stream) {
stream->Start();
system.CoreTiming().ScheduleLoopingEvent(std::chrono::nanoseconds::zero(), INCREMENT_TIME,
thread_event);
}
}
void DeviceSession::Stop() {
if (stream) {
stream->Stop();
system.CoreTiming().UnscheduleEvent(thread_event, {});
}
}
void DeviceSession::ClearBuffers() {
if (stream) {
stream->ClearQueue();
}
}
void DeviceSession::AppendBuffers(std::span<const AudioBuffer> buffers) {
for (const auto& buffer : buffers) {
Sink::SinkBuffer new_buffer{
.frames = buffer.size / (channel_count * sizeof(s16)),
.frames_played = 0,
.tag = buffer.tag,
.consumed = false,
};
tmp_samples.resize_destructive(buffer.size / sizeof(s16));
if (type == Sink::StreamType::In) {
stream->AppendBuffer(new_buffer, tmp_samples);
} else {
Core::Memory::CpuGuestMemory<s16, Core::Memory::GuestMemoryFlags::UnsafeRead> samples(
handle->GetMemory(), buffer.samples, buffer.size / sizeof(s16));
stream->AppendBuffer(new_buffer, samples);
}
}
}
void DeviceSession::ReleaseBuffer(const AudioBuffer& buffer) const {
if (type == Sink::StreamType::In) {
auto samples{stream->ReleaseBuffer(buffer.size / sizeof(s16))};
handle->GetMemory().WriteBlockUnsafe(buffer.samples, samples.data(), buffer.size);
}
}
bool DeviceSession::IsBufferConsumed(const AudioBuffer& buffer) const {
return played_sample_count >= buffer.end_timestamp;
}
void DeviceSession::SetVolume(f32 volume) const {
if (stream) {
stream->SetSystemVolume(volume);
}
}
u64 DeviceSession::GetPlayedSampleCount() const {
return played_sample_count;
}
std::optional<std::chrono::nanoseconds> DeviceSession::ThreadFunc() {
played_sample_count = stream->GetExpectedPlayedSampleCount();
if (type == Sink::StreamType::Out) {
system.AudioCore().GetAudioManager().SetEvent(Event::Type::AudioOutManager, true);
} else {
system.AudioCore().GetAudioManager().SetEvent(Event::Type::AudioInManager, true);
}
return std::nullopt;
}
void DeviceSession::SetRingSize(u32 ring_size) {
stream->SetRingSize(ring_size);
}
} // namespace AudioCore

View File

@@ -0,0 +1,158 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <chrono>
#include <memory>
#include <optional>
#include <span>
#include "audio_core/common/common.h"
#include "audio_core/sink/sink.h"
#include "common/scratch_buffer.h"
#include "core/hle/service/audio/errors.h"
namespace Core {
class System;
namespace Timing {
struct EventType;
} // namespace Timing
} // namespace Core
namespace Kernel {
class KProcess;
} // namespace Kernel
namespace AudioCore {
namespace Sink {
class SinkStream;
struct SinkBuffer;
} // namespace Sink
struct AudioBuffer;
/**
* Represents an input or output device stream for audio in and audio out (not used for render).
**/
class DeviceSession {
public:
explicit DeviceSession(Core::System& system);
~DeviceSession();
/**
* Initialize this device session.
*
* @param name - Name of this device.
* @param sample_format - Sample format for this device's output.
* @param channel_count - Number of channels for this device (2 or 6).
* @param session_id - This session's id.
* @param handle - Process handle for this device session.
* @param applet_resource_user_id - Applet resource user id for this device session (unused).
* @param type - Type of this stream (Render, In, Out).
* @return Result code for this call.
*/
Result Initialize(std::string_view name, SampleFormat sample_format, u16 channel_count,
size_t session_id, Kernel::KProcess* handle, u64 applet_resource_user_id,
Sink::StreamType type);
/**
* Finalize this device session.
*/
void Finalize();
/**
* Append audio buffers to this device session to be played back.
*
* @param buffers - The buffers to play.
*/
void AppendBuffers(std::span<const AudioBuffer> buffers);
/**
* (Audio In only) Pop samples from the backend, and write them back to this buffer's address.
*
* @param buffer - The buffer to write to.
*/
void ReleaseBuffer(const AudioBuffer& buffer) const;
/**
* Check if the buffer for the given tag has been consumed by the backend.
*
* @param buffer - the buffer to check.
*
* @return true if the buffer has been consumed, otherwise false.
*/
bool IsBufferConsumed(const AudioBuffer& buffer) const;
/**
* Start this device session, starting the backend stream.
*/
void Start();
/**
* Stop this device session, stopping the backend stream.
*/
void Stop();
/**
* Clear out the underlying audio buffers in the backend stream.
*/
void ClearBuffers();
/**
* Set this device session's volume.
*
* @param volume - New volume for this session.
*/
void SetVolume(f32 volume) const;
/**
* Get this device session's total played sample count.
*
* @return Samples played by this session.
*/
u64 GetPlayedSampleCount() const;
/*
* CoreTiming callback to increment played_sample_count over time.
*/
std::optional<std::chrono::nanoseconds> ThreadFunc();
/*
* Set the size of the ring buffer.
*/
void SetRingSize(u32 ring_size);
private:
/// System
Core::System& system;
/// Output sink this device will use
Sink::Sink* sink{};
/// The backend stream for this device session to send samples to
Sink::SinkStream* stream{};
/// Name of this device session
std::string name{};
/// Type of this device session (render/in/out)
Sink::StreamType type{};
/// Sample format for this device.
SampleFormat sample_format{SampleFormat::PcmInt16};
/// Channel count for this device session
u16 channel_count{};
/// Session id of this device session
size_t session_id{};
/// Process handle of device memory owner
Kernel::KProcess* handle{};
/// Applet resource user id of this device session
u64 applet_resource_user_id{};
/// Total number of samples played by this device session
std::atomic<u64> played_sample_count{};
/// Event increasing the played sample count every 5ms
std::shared_ptr<Core::Timing::EventType> thread_event;
/// Is this session initialised?
bool initialized{};
/// Temporary sample buffer
Common::ScratchBuffer<s16> tmp_samples{};
};
} // namespace AudioCore

100
src/audio_core/in/audio_in.cpp Executable file
View File

@@ -0,0 +1,100 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/audio_in_manager.h"
#include "audio_core/in/audio_in.h"
#include "core/hle/kernel/k_event.h"
namespace AudioCore::AudioIn {
In::In(Core::System& system_, Manager& manager_, Kernel::KEvent* event_, size_t session_id_)
: manager{manager_}, parent_mutex{manager.mutex}, event{event_}, system{system_, event,
session_id_} {}
void In::Free() {
std::scoped_lock l{parent_mutex};
manager.ReleaseSessionId(system.GetSessionId());
}
System& In::GetSystem() {
return system;
}
AudioIn::State In::GetState() {
std::scoped_lock l{parent_mutex};
return system.GetState();
}
Result In::StartSystem() {
std::scoped_lock l{parent_mutex};
return system.Start();
}
void In::StartSession() {
std::scoped_lock l{parent_mutex};
system.StartSession();
}
Result In::StopSystem() {
std::scoped_lock l{parent_mutex};
return system.Stop();
}
Result In::AppendBuffer(const AudioInBuffer& buffer, u64 tag) {
std::scoped_lock l{parent_mutex};
if (system.AppendBuffer(buffer, tag)) {
return ResultSuccess;
}
return Service::Audio::ResultBufferCountReached;
}
void In::ReleaseAndRegisterBuffers() {
std::scoped_lock l{parent_mutex};
if (system.GetState() == State::Started) {
system.ReleaseBuffers();
system.RegisterBuffers();
}
}
bool In::FlushAudioInBuffers() {
std::scoped_lock l{parent_mutex};
return system.FlushAudioInBuffers();
}
u32 In::GetReleasedBuffers(std::span<u64> tags) {
std::scoped_lock l{parent_mutex};
return system.GetReleasedBuffers(tags);
}
Kernel::KReadableEvent& In::GetBufferEvent() {
std::scoped_lock l{parent_mutex};
return event->GetReadableEvent();
}
f32 In::GetVolume() const {
std::scoped_lock l{parent_mutex};
return system.GetVolume();
}
void In::SetVolume(f32 volume) {
std::scoped_lock l{parent_mutex};
system.SetVolume(volume);
}
bool In::ContainsAudioBuffer(u64 tag) const {
std::scoped_lock l{parent_mutex};
return system.ContainsAudioBuffer(tag);
}
u32 In::GetBufferCount() const {
std::scoped_lock l{parent_mutex};
return system.GetBufferCount();
}
u64 In::GetPlayedSampleCount() const {
std::scoped_lock l{parent_mutex};
return system.GetPlayedSampleCount();
}
} // namespace AudioCore::AudioIn

147
src/audio_core/in/audio_in.h Executable file
View File

@@ -0,0 +1,147 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <mutex>
#include "audio_core/in/audio_in_system.h"
namespace Core {
class System;
}
namespace Kernel {
class KEvent;
class KReadableEvent;
} // namespace Kernel
namespace AudioCore::AudioIn {
class Manager;
/**
* Interface between the service and audio in system. Mainly responsible for forwarding service
* calls to the system.
*/
class In {
public:
explicit In(Core::System& system, Manager& manager, Kernel::KEvent* event, size_t session_id);
/**
* Free this audio in from the audio in manager.
*/
void Free();
/**
* Get this audio in's system.
*/
System& GetSystem();
/**
* Get the current state.
*
* @return Started or Stopped.
*/
AudioIn::State GetState();
/**
* Start the system
*
* @return Result code
*/
Result StartSystem();
/**
* Start the system's device session.
*/
void StartSession();
/**
* Stop the system.
*
* @return Result code
*/
Result StopSystem();
/**
* Append a new buffer to the system, the buffer event will be signalled when it is filled.
*
* @param buffer - The new buffer to append.
* @param tag - Unique tag for this buffer.
* @return Result code.
*/
Result AppendBuffer(const AudioInBuffer& buffer, u64 tag);
/**
* Release all completed buffers, and register any appended.
*/
void ReleaseAndRegisterBuffers();
/**
* Flush all buffers.
*/
bool FlushAudioInBuffers();
/**
* Get all of the currently released buffers.
*
* @param tags - Output container for the buffer tags which were released.
* @return The number of buffers released.
*/
u32 GetReleasedBuffers(std::span<u64> tags);
/**
* Get the buffer event for this audio in, this event will be signalled when a buffer is filled.
*
* @return The buffer event.
*/
Kernel::KReadableEvent& GetBufferEvent();
/**
* Get the current system volume.
*
* @return The current volume.
*/
f32 GetVolume() const;
/**
* Set the system volume.
*
* @param volume - The volume to set.
*/
void SetVolume(f32 volume);
/**
* Check if a buffer is in the system.
*
* @param tag - The tag to search for.
* @return True if the buffer is in the system, otherwise false.
*/
bool ContainsAudioBuffer(u64 tag) const;
/**
* Get the maximum number of buffers.
*
* @return The maximum number of buffers.
*/
u32 GetBufferCount() const;
/**
* Get the total played sample count for this audio in.
*
* @return The played sample count.
*/
u64 GetPlayedSampleCount() const;
private:
/// The AudioIn::Manager this audio in is registered with
Manager& manager;
/// Manager's mutex
std::recursive_mutex& parent_mutex;
/// Buffer event, signalled when buffers are ready to be released
Kernel::KEvent* event;
/// Main audio in system
System system;
};
} // namespace AudioCore::AudioIn

View File

@@ -0,0 +1,225 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <mutex>
#include "audio_core/audio_event.h"
#include "audio_core/audio_manager.h"
#include "audio_core/in/audio_in_system.h"
#include "common/logging/log.h"
#include "core/core.h"
#include "core/core_timing.h"
#include "core/hle/kernel/k_event.h"
namespace AudioCore::AudioIn {
System::System(Core::System& system_, Kernel::KEvent* event_, const size_t session_id_)
: system{system_}, buffer_event{event_},
session_id{session_id_}, session{std::make_unique<DeviceSession>(system_)} {}
System::~System() {
Finalize();
}
void System::Finalize() {
Stop();
session->Finalize();
}
void System::StartSession() {
session->Start();
}
size_t System::GetSessionId() const {
return session_id;
}
std::string_view System::GetDefaultDeviceName() const {
return "BuiltInHeadset";
}
std::string_view System::GetDefaultUacDeviceName() const {
return "Uac";
}
Result System::IsConfigValid(const std::string_view device_name,
const AudioInParameter& in_params) const {
if ((device_name.size() > 0) &&
(device_name != GetDefaultDeviceName() && device_name != GetDefaultUacDeviceName())) {
return Service::Audio::ResultNotFound;
}
if (in_params.sample_rate != TargetSampleRate && in_params.sample_rate > 0) {
return Service::Audio::ResultInvalidSampleRate;
}
return ResultSuccess;
}
Result System::Initialize(std::string device_name, const AudioInParameter& in_params,
Kernel::KProcess* handle_, const u64 applet_resource_user_id_) {
auto result{IsConfigValid(device_name, in_params)};
if (result.IsError()) {
return result;
}
handle = handle_;
applet_resource_user_id = applet_resource_user_id_;
if (device_name.empty() || device_name[0] == '\0') {
name = std::string(GetDefaultDeviceName());
} else {
name = std::move(device_name);
}
sample_rate = TargetSampleRate;
sample_format = SampleFormat::PcmInt16;
channel_count = in_params.channel_count <= 2 ? 2 : 6;
volume = 1.0f;
is_uac = name == "Uac";
return ResultSuccess;
}
Result System::Start() {
if (state != State::Stopped) {
return Service::Audio::ResultOperationFailed;
}
session->Initialize(name, sample_format, channel_count, session_id, handle,
applet_resource_user_id, Sink::StreamType::In);
session->SetVolume(volume);
session->Start();
state = State::Started;
boost::container::static_vector<AudioBuffer, BufferCount> buffers_to_flush{};
buffers.RegisterBuffers(buffers_to_flush);
session->AppendBuffers(buffers_to_flush);
session->SetRingSize(static_cast<u32>(buffers_to_flush.size()));
return ResultSuccess;
}
Result System::Stop() {
if (state == State::Started) {
session->Stop();
session->SetVolume(0.0f);
session->ClearBuffers();
if (buffers.ReleaseBuffers(system.CoreTiming(), *session, true)) {
buffer_event->Signal();
}
state = State::Stopped;
}
return ResultSuccess;
}
bool System::AppendBuffer(const AudioInBuffer& buffer, const u64 tag) {
if (buffers.GetTotalBufferCount() == BufferCount) {
return false;
}
const auto timestamp{buffers.GetNextTimestamp()};
const AudioBuffer new_buffer{
.start_timestamp = timestamp,
.end_timestamp = timestamp + buffer.size / (channel_count * sizeof(s16)),
.played_timestamp = 0,
.samples = buffer.samples,
.tag = tag,
.size = buffer.size,
};
buffers.AppendBuffer(new_buffer);
RegisterBuffers();
return true;
}
void System::RegisterBuffers() {
if (state == State::Started) {
boost::container::static_vector<AudioBuffer, BufferCount> registered_buffers{};
buffers.RegisterBuffers(registered_buffers);
session->AppendBuffers(registered_buffers);
}
}
void System::ReleaseBuffers() {
bool signal{buffers.ReleaseBuffers(system.CoreTiming(), *session, false)};
if (signal) {
// Signal if any buffer was released, or if none are registered, we need more.
buffer_event->Signal();
}
}
u32 System::GetReleasedBuffers(std::span<u64> tags) {
return buffers.GetReleasedBuffers(tags);
}
bool System::FlushAudioInBuffers() {
if (state != State::Started) {
return false;
}
u32 buffers_released{};
buffers.FlushBuffers(buffers_released);
if (buffers_released > 0) {
buffer_event->Signal();
}
return true;
}
u16 System::GetChannelCount() const {
return channel_count;
}
u32 System::GetSampleRate() const {
return sample_rate;
}
SampleFormat System::GetSampleFormat() const {
return sample_format;
}
State System::GetState() {
switch (state) {
case State::Started:
case State::Stopped:
return state;
default:
LOG_ERROR(Service_Audio, "AudioIn invalid state!");
state = State::Stopped;
break;
}
return state;
}
std::string System::GetName() const {
return name;
}
f32 System::GetVolume() const {
return volume;
}
void System::SetVolume(const f32 volume_) {
volume = volume_;
session->SetVolume(volume_);
}
bool System::ContainsAudioBuffer(const u64 tag) const {
return buffers.ContainsBuffer(tag);
}
u32 System::GetBufferCount() const {
return buffers.GetAppendedRegisteredCount();
}
u64 System::GetPlayedSampleCount() const {
return session->GetPlayedSampleCount();
}
bool System::IsUac() const {
return is_uac;
}
} // namespace AudioCore::AudioIn

View File

@@ -0,0 +1,276 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <memory>
#include <span>
#include <string>
#include "audio_core/common/common.h"
#include "audio_core/device/audio_buffers.h"
#include "audio_core/device/device_session.h"
#include "core/hle/service/audio/errors.h"
namespace Core {
class System;
}
namespace Kernel {
class KEvent;
class KProcess;
} // namespace Kernel
namespace AudioCore::AudioIn {
constexpr SessionTypes SessionType = SessionTypes::AudioIn;
struct AudioInParameter {
/* 0x0 */ s32_le sample_rate;
/* 0x4 */ u16_le channel_count;
/* 0x6 */ u16_le reserved;
};
static_assert(sizeof(AudioInParameter) == 0x8, "AudioInParameter is an invalid size");
struct AudioInParameterInternal {
/* 0x0 */ u32_le sample_rate;
/* 0x4 */ u32_le channel_count;
/* 0x8 */ u32_le sample_format;
/* 0xC */ u32_le state;
};
static_assert(sizeof(AudioInParameterInternal) == 0x10,
"AudioInParameterInternal is an invalid size");
struct AudioInBuffer {
/* 0x00 */ AudioInBuffer* next;
/* 0x08 */ VAddr samples;
/* 0x10 */ u64 capacity;
/* 0x18 */ u64 size;
/* 0x20 */ u64 offset;
};
static_assert(sizeof(AudioInBuffer) == 0x28, "AudioInBuffer is an invalid size");
enum class State {
Started,
Stopped,
};
/**
* Controls and drives audio input.
*/
class System {
public:
explicit System(Core::System& system, Kernel::KEvent* event, size_t session_id);
~System();
/**
* Get the default audio input device name.
*
* @return The default audio input device name.
*/
std::string_view GetDefaultDeviceName() const;
/**
* Get the default USB audio input device name.
* This is preferred over non-USB as some games refuse to work with the BuiltInHeadset
* (e.g Let's Sing).
*
* @return The default USB audio input device name.
*/
std::string_view GetDefaultUacDeviceName() const;
/**
* Is the given initialize config valid?
*
* @param device_name - The name of the requested input device.
* @param in_params - Input parameters, see AudioInParameter.
* @return Result code.
*/
Result IsConfigValid(std::string_view device_name, const AudioInParameter& in_params) const;
/**
* Initialize this system.
*
* @param device_name - The name of the requested input device.
* @param in_params - Input parameters, see AudioInParameter.
* @param handle - Process handle.
* @param applet_resource_user_id - Unused.
* @return Result code.
*/
Result Initialize(std::string device_name, const AudioInParameter& in_params,
Kernel::KProcess* handle, u64 applet_resource_user_id);
/**
* Start this system.
*
* @return Result code.
*/
Result Start();
/**
* Stop this system.
*
* @return Result code.
*/
Result Stop();
/**
* Finalize this system.
*/
void Finalize();
/**
* Start this system's device session.
*/
void StartSession();
/**
* Get this system's id.
*/
size_t GetSessionId() const;
/**
* Append a new buffer to the device.
*
* @param buffer - New buffer to append.
* @param tag - Unique tag of the buffer.
* @return True if the buffer was appended, otherwise false.
*/
bool AppendBuffer(const AudioInBuffer& buffer, u64 tag);
/**
* Register all appended buffers.
*/
void RegisterBuffers();
/**
* Release all registered buffers.
*/
void ReleaseBuffers();
/**
* Get all released buffers.
*
* @param tags - Container to be filled with the released buffers' tags.
* @return The number of buffers released.
*/
u32 GetReleasedBuffers(std::span<u64> tags);
/**
* Flush all appended and registered buffers.
*
* @return True if buffers were successfully flushed, otherwise false.
*/
bool FlushAudioInBuffers();
/**
* Get this system's current channel count.
*
* @return The channel count.
*/
u16 GetChannelCount() const;
/**
* Get this system's current sample rate.
*
* @return The sample rate.
*/
u32 GetSampleRate() const;
/**
* Get this system's current sample format.
*
* @return The sample format.
*/
SampleFormat GetSampleFormat() const;
/**
* Get this system's current state.
*
* @return The current state.
*/
State GetState();
/**
* Get this system's name.
*
* @return The system's name.
*/
std::string GetName() const;
/**
* Get this system's current volume.
*
* @return The system's current volume.
*/
f32 GetVolume() const;
/**
* Set this system's current volume.
*
* @param volume The new volume.
*/
void SetVolume(f32 volume);
/**
* Does the system contain this buffer?
*
* @param tag - Unique tag to search for.
* @return True if the buffer is in the system, otherwise false.
*/
bool ContainsAudioBuffer(u64 tag) const;
/**
* Get the maximum number of usable buffers (default 32).
*
* @return The number of buffers.
*/
u32 GetBufferCount() const;
/**
* Get the total number of samples played by this system.
*
* @return The number of samples.
*/
u64 GetPlayedSampleCount() const;
/**
* Is this system using a USB device?
*
* @return True if using a USB device, otherwise false.
*/
bool IsUac() const;
private:
/// Core system
Core::System& system;
/// Process handle
Kernel::KProcess* handle{};
/// (Unused)
u64 applet_resource_user_id{};
/// Buffer event, signalled when a buffer is ready
Kernel::KEvent* buffer_event;
/// Session id of this system
size_t session_id{};
/// Device session for this system
std::unique_ptr<DeviceSession> session;
/// Audio buffers in use by this system
AudioBuffers<BufferCount> buffers{BufferCount};
/// Sample rate of this system
u32 sample_rate{};
/// Sample format of this system
SampleFormat sample_format{SampleFormat::PcmInt16};
/// Channel count of this system
u16 channel_count{};
/// State of this system
std::atomic<State> state{State::Stopped};
/// Name of this system
std::string name{};
/// Volume of this system
f32 volume{1.0f};
/// Is this system's device USB?
bool is_uac{false};
};
} // namespace AudioCore::AudioIn

179
src/audio_core/opus/decoder.cpp Executable file
View File

@@ -0,0 +1,179 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/opus/decoder.h"
#include "audio_core/opus/hardware_opus.h"
#include "audio_core/opus/parameters.h"
#include "common/alignment.h"
#include "common/swap.h"
#include "core/core.h"
namespace AudioCore::OpusDecoder {
using namespace Service::Audio;
namespace {
OpusPacketHeader ReverseHeader(OpusPacketHeader header) {
OpusPacketHeader out;
out.size = Common::swap32(header.size);
out.final_range = Common::swap32(header.final_range);
return out;
}
} // namespace
OpusDecoder::OpusDecoder(Core::System& system_, HardwareOpus& hardware_opus_)
: system{system_}, hardware_opus{hardware_opus_} {}
OpusDecoder::~OpusDecoder() {
if (decode_object_initialized) {
hardware_opus.ShutdownDecodeObject(shared_buffer.get(), shared_buffer_size);
}
}
Result OpusDecoder::Initialize(const OpusParametersEx& params,
Kernel::KTransferMemory* transfer_memory, u64 transfer_memory_size) {
auto frame_size{params.use_large_frame_size ? 5760 : 1920};
shared_buffer_size = transfer_memory_size;
shared_buffer = std::make_unique<u8[]>(shared_buffer_size);
shared_memory_mapped = true;
buffer_size =
Common::AlignUp((frame_size * params.channel_count) / (48'000 / params.sample_rate), 16);
out_data = {shared_buffer.get() + shared_buffer_size - buffer_size, buffer_size};
size_t in_data_size{0x600u};
in_data = {out_data.data() - in_data_size, in_data_size};
ON_RESULT_FAILURE {
if (shared_memory_mapped) {
shared_memory_mapped = false;
ASSERT(R_SUCCEEDED(hardware_opus.UnmapMemory(shared_buffer.get(), shared_buffer_size)));
}
};
R_TRY(hardware_opus.InitializeDecodeObject(params.sample_rate, params.channel_count,
shared_buffer.get(), shared_buffer_size));
sample_rate = params.sample_rate;
channel_count = params.channel_count;
use_large_frame_size = params.use_large_frame_size;
decode_object_initialized = true;
R_SUCCEED();
}
Result OpusDecoder::Initialize(const OpusMultiStreamParametersEx& params,
Kernel::KTransferMemory* transfer_memory, u64 transfer_memory_size) {
auto frame_size{params.use_large_frame_size ? 5760 : 1920};
shared_buffer_size = transfer_memory_size;
shared_buffer = std::make_unique<u8[]>(shared_buffer_size);
shared_memory_mapped = true;
buffer_size =
Common::AlignUp((frame_size * params.channel_count) / (48'000 / params.sample_rate), 16);
out_data = {shared_buffer.get() + shared_buffer_size - buffer_size, buffer_size};
size_t in_data_size{Common::AlignUp(1500ull * params.total_stream_count, 64u)};
in_data = {out_data.data() - in_data_size, in_data_size};
ON_RESULT_FAILURE {
if (shared_memory_mapped) {
shared_memory_mapped = false;
ASSERT(R_SUCCEEDED(hardware_opus.UnmapMemory(shared_buffer.get(), shared_buffer_size)));
}
};
R_TRY(hardware_opus.InitializeMultiStreamDecodeObject(
params.sample_rate, params.channel_count, params.total_stream_count,
params.stereo_stream_count, params.mappings.data(), shared_buffer.get(),
shared_buffer_size));
sample_rate = params.sample_rate;
channel_count = params.channel_count;
total_stream_count = params.total_stream_count;
stereo_stream_count = params.stereo_stream_count;
use_large_frame_size = params.use_large_frame_size;
decode_object_initialized = true;
R_SUCCEED();
}
Result OpusDecoder::DecodeInterleaved(u32* out_data_size, u64* out_time_taken,
u32* out_sample_count, std::span<const u8> input_data,
std::span<u8> output_data, bool reset) {
u32 out_samples;
u64 time_taken{};
R_UNLESS(input_data.size_bytes() > sizeof(OpusPacketHeader), ResultInputDataTooSmall);
auto* header_p{reinterpret_cast<const OpusPacketHeader*>(input_data.data())};
OpusPacketHeader header{ReverseHeader(*header_p)};
R_UNLESS(in_data.size_bytes() >= header.size &&
header.size + sizeof(OpusPacketHeader) <= input_data.size_bytes(),
ResultBufferTooSmall);
if (!shared_memory_mapped) {
R_TRY(hardware_opus.MapMemory(shared_buffer.get(), shared_buffer_size));
shared_memory_mapped = true;
}
std::memcpy(in_data.data(), input_data.data() + sizeof(OpusPacketHeader), header.size);
R_TRY(hardware_opus.DecodeInterleaved(out_samples, out_data.data(), out_data.size_bytes(),
channel_count, in_data.data(), header.size,
shared_buffer.get(), time_taken, reset));
std::memcpy(output_data.data(), out_data.data(), out_samples * channel_count * sizeof(s16));
*out_data_size = header.size + sizeof(OpusPacketHeader);
*out_sample_count = out_samples;
if (out_time_taken) {
*out_time_taken = time_taken / 1000;
}
R_SUCCEED();
}
Result OpusDecoder::SetContext([[maybe_unused]] std::span<const u8> context) {
R_SUCCEED_IF(shared_memory_mapped);
shared_memory_mapped = true;
R_RETURN(hardware_opus.MapMemory(shared_buffer.get(), shared_buffer_size));
}
Result OpusDecoder::DecodeInterleavedForMultiStream(u32* out_data_size, u64* out_time_taken,
u32* out_sample_count,
std::span<const u8> input_data,
std::span<u8> output_data, bool reset) {
u32 out_samples;
u64 time_taken{};
R_UNLESS(input_data.size_bytes() > sizeof(OpusPacketHeader), ResultInputDataTooSmall);
auto* header_p{reinterpret_cast<const OpusPacketHeader*>(input_data.data())};
OpusPacketHeader header{ReverseHeader(*header_p)};
LOG_TRACE(Service_Audio, "header size 0x{:X} input data size 0x{:X} in_data size 0x{:X}",
header.size, input_data.size_bytes(), in_data.size_bytes());
R_UNLESS(in_data.size_bytes() >= header.size &&
header.size + sizeof(OpusPacketHeader) <= input_data.size_bytes(),
ResultBufferTooSmall);
if (!shared_memory_mapped) {
R_TRY(hardware_opus.MapMemory(shared_buffer.get(), shared_buffer_size));
shared_memory_mapped = true;
}
std::memcpy(in_data.data(), input_data.data() + sizeof(OpusPacketHeader), header.size);
R_TRY(hardware_opus.DecodeInterleavedForMultiStream(
out_samples, out_data.data(), out_data.size_bytes(), channel_count, in_data.data(),
header.size, shared_buffer.get(), time_taken, reset));
std::memcpy(output_data.data(), out_data.data(), out_samples * channel_count * sizeof(s16));
*out_data_size = header.size + sizeof(OpusPacketHeader);
*out_sample_count = out_samples;
if (out_time_taken) {
*out_time_taken = time_taken / 1000;
}
R_SUCCEED();
}
} // namespace AudioCore::OpusDecoder

53
src/audio_core/opus/decoder.h Executable file
View File

@@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <span>
#include "audio_core/opus/parameters.h"
#include "common/common_types.h"
#include "core/hle/kernel/k_transfer_memory.h"
#include "core/hle/service/audio/errors.h"
namespace Core {
class System;
}
namespace AudioCore::OpusDecoder {
class HardwareOpus;
class OpusDecoder {
public:
explicit OpusDecoder(Core::System& system, HardwareOpus& hardware_opus_);
~OpusDecoder();
Result Initialize(const OpusParametersEx& params, Kernel::KTransferMemory* transfer_memory,
u64 transfer_memory_size);
Result Initialize(const OpusMultiStreamParametersEx& params,
Kernel::KTransferMemory* transfer_memory, u64 transfer_memory_size);
Result DecodeInterleaved(u32* out_data_size, u64* out_time_taken, u32* out_sample_count,
std::span<const u8> input_data, std::span<u8> output_data, bool reset);
Result SetContext([[maybe_unused]] std::span<const u8> context);
Result DecodeInterleavedForMultiStream(u32* out_data_size, u64* out_time_taken,
u32* out_sample_count, std::span<const u8> input_data,
std::span<u8> output_data, bool reset);
private:
Core::System& system;
HardwareOpus& hardware_opus;
std::unique_ptr<u8[]> shared_buffer{};
u64 shared_buffer_size;
std::span<u8> in_data{};
std::span<u8> out_data{};
u64 buffer_size{};
s32 sample_rate{};
s32 channel_count{};
bool use_large_frame_size{false};
s32 total_stream_count{};
s32 stereo_stream_count{};
bool shared_memory_mapped{false};
bool decode_object_initialized{false};
};
} // namespace AudioCore::OpusDecoder

View File

@@ -0,0 +1,102 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/adsp/apps/opus/opus_decoder.h"
#include "audio_core/opus/decoder_manager.h"
#include "common/alignment.h"
#include "core/core.h"
namespace AudioCore::OpusDecoder {
using namespace Service::Audio;
namespace {
bool IsValidChannelCount(u32 channel_count) {
return channel_count == 1 || channel_count == 2;
}
bool IsValidMultiStreamChannelCount(u32 channel_count) {
return channel_count > 0 && channel_count <= OpusStreamCountMax;
}
bool IsValidSampleRate(u32 sample_rate) {
return sample_rate == 8'000 || sample_rate == 12'000 || sample_rate == 16'000 ||
sample_rate == 24'000 || sample_rate == 48'000;
}
bool IsValidStreamCount(u32 channel_count, u32 total_stream_count, u32 stereo_stream_count) {
return total_stream_count > 0 && static_cast<s32>(stereo_stream_count) >= 0 &&
stereo_stream_count <= total_stream_count &&
total_stream_count + stereo_stream_count <= channel_count;
}
} // namespace
OpusDecoderManager::OpusDecoderManager(Core::System& system_)
: system{system_}, hardware_opus{system} {
for (u32 i = 0; i < MaxChannels; i++) {
required_workbuffer_sizes[i] = hardware_opus.GetWorkBufferSize(1 + i);
}
}
Result OpusDecoderManager::GetWorkBufferSize(const OpusParameters& params, u32& out_size) {
OpusParametersEx ex{
.sample_rate = params.sample_rate,
.channel_count = params.channel_count,
.use_large_frame_size = false,
};
R_RETURN(GetWorkBufferSizeExEx(ex, out_size));
}
Result OpusDecoderManager::GetWorkBufferSizeEx(const OpusParametersEx& params, u32& out_size) {
R_RETURN(GetWorkBufferSizeExEx(params, out_size));
}
Result OpusDecoderManager::GetWorkBufferSizeExEx(const OpusParametersEx& params, u32& out_size) {
R_UNLESS(IsValidChannelCount(params.channel_count), ResultInvalidOpusChannelCount);
R_UNLESS(IsValidSampleRate(params.sample_rate), ResultInvalidOpusSampleRate);
auto work_buffer_size{required_workbuffer_sizes[params.channel_count - 1]};
auto frame_size{params.use_large_frame_size ? 5760 : 1920};
work_buffer_size +=
Common::AlignUp((frame_size * params.channel_count) / (48'000 / params.sample_rate), 64);
out_size = work_buffer_size + 0x600;
R_SUCCEED();
}
Result OpusDecoderManager::GetWorkBufferSizeForMultiStream(const OpusMultiStreamParameters& params,
u32& out_size) {
OpusMultiStreamParametersEx ex{
.sample_rate = params.sample_rate,
.channel_count = params.channel_count,
.total_stream_count = params.total_stream_count,
.stereo_stream_count = params.stereo_stream_count,
.use_large_frame_size = false,
.mappings = {},
};
R_RETURN(GetWorkBufferSizeForMultiStreamExEx(ex, out_size));
}
Result OpusDecoderManager::GetWorkBufferSizeForMultiStreamEx(
const OpusMultiStreamParametersEx& params, u32& out_size) {
R_RETURN(GetWorkBufferSizeForMultiStreamExEx(params, out_size));
}
Result OpusDecoderManager::GetWorkBufferSizeForMultiStreamExEx(
const OpusMultiStreamParametersEx& params, u32& out_size) {
R_UNLESS(IsValidMultiStreamChannelCount(params.channel_count), ResultInvalidOpusChannelCount);
R_UNLESS(IsValidSampleRate(params.sample_rate), ResultInvalidOpusSampleRate);
R_UNLESS(IsValidStreamCount(params.channel_count, params.total_stream_count,
params.stereo_stream_count),
ResultInvalidOpusSampleRate);
auto work_buffer_size{hardware_opus.GetWorkBufferSizeForMultiStream(
params.total_stream_count, params.stereo_stream_count)};
auto frame_size{params.use_large_frame_size ? 5760 : 1920};
work_buffer_size += Common::AlignUp(1500 * params.total_stream_count, 64);
work_buffer_size +=
Common::AlignUp((frame_size * params.channel_count) / (48'000 / params.sample_rate), 64);
out_size = work_buffer_size;
R_SUCCEED();
}
} // namespace AudioCore::OpusDecoder

View File

@@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "audio_core/opus/hardware_opus.h"
#include "audio_core/opus/parameters.h"
#include "common/common_types.h"
#include "core/hle/service/audio/errors.h"
namespace Core {
class System;
}
namespace AudioCore::OpusDecoder {
class OpusDecoderManager {
public:
OpusDecoderManager(Core::System& system);
HardwareOpus& GetHardwareOpus() {
return hardware_opus;
}
Result GetWorkBufferSize(const OpusParameters& params, u32& out_size);
Result GetWorkBufferSizeEx(const OpusParametersEx& params, u32& out_size);
Result GetWorkBufferSizeExEx(const OpusParametersEx& params, u32& out_size);
Result GetWorkBufferSizeForMultiStream(const OpusMultiStreamParameters& params, u32& out_size);
Result GetWorkBufferSizeForMultiStreamEx(const OpusMultiStreamParametersEx& params,
u32& out_size);
Result GetWorkBufferSizeForMultiStreamExEx(const OpusMultiStreamParametersEx& params,
u32& out_size);
private:
Core::System& system;
HardwareOpus hardware_opus;
std::array<u32, MaxChannels> required_workbuffer_sizes{};
};
} // namespace AudioCore::OpusDecoder

View File

@@ -0,0 +1,242 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <array>
#include "audio_core/audio_core.h"
#include "audio_core/opus/hardware_opus.h"
#include "core/core.h"
namespace AudioCore::OpusDecoder {
namespace {
using namespace Service::Audio;
static constexpr Result ResultCodeFromLibOpusErrorCode(u64 error_code) {
s32 error{static_cast<s32>(error_code)};
ASSERT(error <= OPUS_OK);
switch (error) {
case OPUS_ALLOC_FAIL:
R_THROW(ResultLibOpusAllocFail);
case OPUS_INVALID_STATE:
R_THROW(ResultLibOpusInvalidState);
case OPUS_UNIMPLEMENTED:
R_THROW(ResultLibOpusUnimplemented);
case OPUS_INVALID_PACKET:
R_THROW(ResultLibOpusInvalidPacket);
case OPUS_INTERNAL_ERROR:
R_THROW(ResultLibOpusInternalError);
case OPUS_BUFFER_TOO_SMALL:
R_THROW(ResultBufferTooSmall);
case OPUS_BAD_ARG:
R_THROW(ResultLibOpusBadArg);
case OPUS_OK:
R_RETURN(ResultSuccess);
}
UNREACHABLE();
}
} // namespace
HardwareOpus::HardwareOpus(Core::System& system_)
: system{system_}, opus_decoder{system.AudioCore().ADSP().OpusDecoder()} {
opus_decoder.SetSharedMemory(shared_memory);
}
u32 HardwareOpus::GetWorkBufferSize(u32 channel) {
if (!opus_decoder.IsRunning()) {
return 0;
}
std::scoped_lock l{mutex};
shared_memory.host_send_data[0] = channel;
opus_decoder.Send(ADSP::Direction::DSP, ADSP::OpusDecoder::Message::GetWorkBufferSize);
auto msg = opus_decoder.Receive(ADSP::Direction::Host);
if (msg != ADSP::OpusDecoder::Message::GetWorkBufferSizeOK) {
LOG_ERROR(Service_Audio, "OpusDecoder returned invalid message. Expected {} got {}",
ADSP::OpusDecoder::Message::GetWorkBufferSizeOK, msg);
return 0;
}
return static_cast<u32>(shared_memory.dsp_return_data[0]);
}
u32 HardwareOpus::GetWorkBufferSizeForMultiStream(u32 total_stream_count, u32 stereo_stream_count) {
std::scoped_lock l{mutex};
shared_memory.host_send_data[0] = total_stream_count;
shared_memory.host_send_data[1] = stereo_stream_count;
opus_decoder.Send(ADSP::Direction::DSP,
ADSP::OpusDecoder::Message::GetWorkBufferSizeForMultiStream);
auto msg = opus_decoder.Receive(ADSP::Direction::Host);
if (msg != ADSP::OpusDecoder::Message::GetWorkBufferSizeForMultiStreamOK) {
LOG_ERROR(Service_Audio, "OpusDecoder returned invalid message. Expected {} got {}",
ADSP::OpusDecoder::Message::GetWorkBufferSizeForMultiStreamOK, msg);
return 0;
}
return static_cast<u32>(shared_memory.dsp_return_data[0]);
}
Result HardwareOpus::InitializeDecodeObject(u32 sample_rate, u32 channel_count, void* buffer,
u64 buffer_size) {
std::scoped_lock l{mutex};
shared_memory.host_send_data[0] = (u64)buffer;
shared_memory.host_send_data[1] = buffer_size;
shared_memory.host_send_data[2] = sample_rate;
shared_memory.host_send_data[3] = channel_count;
opus_decoder.Send(ADSP::Direction::DSP, ADSP::OpusDecoder::Message::InitializeDecodeObject);
auto msg = opus_decoder.Receive(ADSP::Direction::Host);
if (msg != ADSP::OpusDecoder::Message::InitializeDecodeObjectOK) {
LOG_ERROR(Service_Audio, "OpusDecoder returned invalid message. Expected {} got {}",
ADSP::OpusDecoder::Message::InitializeDecodeObjectOK, msg);
R_THROW(ResultInvalidOpusDSPReturnCode);
}
R_RETURN(ResultCodeFromLibOpusErrorCode(shared_memory.dsp_return_data[0]));
}
Result HardwareOpus::InitializeMultiStreamDecodeObject(u32 sample_rate, u32 channel_count,
u32 total_stream_count,
u32 stereo_stream_count,
const void* mappings, void* buffer,
u64 buffer_size) {
std::scoped_lock l{mutex};
shared_memory.host_send_data[0] = (u64)buffer;
shared_memory.host_send_data[1] = buffer_size;
shared_memory.host_send_data[2] = sample_rate;
shared_memory.host_send_data[3] = channel_count;
shared_memory.host_send_data[4] = total_stream_count;
shared_memory.host_send_data[5] = stereo_stream_count;
ASSERT(channel_count <= MaxChannels);
std::memcpy(shared_memory.channel_mapping.data(), mappings, channel_count * sizeof(u8));
opus_decoder.Send(ADSP::Direction::DSP,
ADSP::OpusDecoder::Message::InitializeMultiStreamDecodeObject);
auto msg = opus_decoder.Receive(ADSP::Direction::Host);
if (msg != ADSP::OpusDecoder::Message::InitializeMultiStreamDecodeObjectOK) {
LOG_ERROR(Service_Audio, "OpusDecoder returned invalid message. Expected {} got {}",
ADSP::OpusDecoder::Message::InitializeMultiStreamDecodeObjectOK, msg);
R_THROW(ResultInvalidOpusDSPReturnCode);
}
R_RETURN(ResultCodeFromLibOpusErrorCode(shared_memory.dsp_return_data[0]));
}
Result HardwareOpus::ShutdownDecodeObject(void* buffer, u64 buffer_size) {
std::scoped_lock l{mutex};
shared_memory.host_send_data[0] = (u64)buffer;
shared_memory.host_send_data[1] = buffer_size;
opus_decoder.Send(ADSP::Direction::DSP, ADSP::OpusDecoder::Message::ShutdownDecodeObject);
auto msg = opus_decoder.Receive(ADSP::Direction::Host);
ASSERT_MSG(msg == ADSP::OpusDecoder::Message::ShutdownDecodeObjectOK,
"Expected Opus shutdown code {}, got {}",
ADSP::OpusDecoder::Message::ShutdownDecodeObjectOK, msg);
R_RETURN(ResultCodeFromLibOpusErrorCode(shared_memory.dsp_return_data[0]));
}
Result HardwareOpus::ShutdownMultiStreamDecodeObject(void* buffer, u64 buffer_size) {
std::scoped_lock l{mutex};
shared_memory.host_send_data[0] = (u64)buffer;
shared_memory.host_send_data[1] = buffer_size;
opus_decoder.Send(ADSP::Direction::DSP,
ADSP::OpusDecoder::Message::ShutdownMultiStreamDecodeObject);
auto msg = opus_decoder.Receive(ADSP::Direction::Host);
ASSERT_MSG(msg == ADSP::OpusDecoder::Message::ShutdownMultiStreamDecodeObjectOK,
"Expected Opus shutdown code {}, got {}",
ADSP::OpusDecoder::Message::ShutdownMultiStreamDecodeObjectOK, msg);
R_RETURN(ResultCodeFromLibOpusErrorCode(shared_memory.dsp_return_data[0]));
}
Result HardwareOpus::DecodeInterleaved(u32& out_sample_count, void* output_data,
u64 output_data_size, u32 channel_count, void* input_data,
u64 input_data_size, void* buffer, u64& out_time_taken,
bool reset) {
std::scoped_lock l{mutex};
shared_memory.host_send_data[0] = (u64)buffer;
shared_memory.host_send_data[1] = (u64)input_data;
shared_memory.host_send_data[2] = input_data_size;
shared_memory.host_send_data[3] = (u64)output_data;
shared_memory.host_send_data[4] = output_data_size;
shared_memory.host_send_data[5] = 0;
shared_memory.host_send_data[6] = reset;
opus_decoder.Send(ADSP::Direction::DSP, ADSP::OpusDecoder::Message::DecodeInterleaved);
auto msg = opus_decoder.Receive(ADSP::Direction::Host);
if (msg != ADSP::OpusDecoder::Message::DecodeInterleavedOK) {
LOG_ERROR(Service_Audio, "OpusDecoder returned invalid message. Expected {} got {}",
ADSP::OpusDecoder::Message::DecodeInterleavedOK, msg);
R_THROW(ResultInvalidOpusDSPReturnCode);
}
auto error_code{static_cast<s32>(shared_memory.dsp_return_data[0])};
if (error_code == OPUS_OK) {
out_sample_count = static_cast<u32>(shared_memory.dsp_return_data[1]);
out_time_taken = 1000 * shared_memory.dsp_return_data[2];
}
R_RETURN(ResultCodeFromLibOpusErrorCode(error_code));
}
Result HardwareOpus::DecodeInterleavedForMultiStream(u32& out_sample_count, void* output_data,
u64 output_data_size, u32 channel_count,
void* input_data, u64 input_data_size,
void* buffer, u64& out_time_taken,
bool reset) {
std::scoped_lock l{mutex};
shared_memory.host_send_data[0] = (u64)buffer;
shared_memory.host_send_data[1] = (u64)input_data;
shared_memory.host_send_data[2] = input_data_size;
shared_memory.host_send_data[3] = (u64)output_data;
shared_memory.host_send_data[4] = output_data_size;
shared_memory.host_send_data[5] = 0;
shared_memory.host_send_data[6] = reset;
opus_decoder.Send(ADSP::Direction::DSP,
ADSP::OpusDecoder::Message::DecodeInterleavedForMultiStream);
auto msg = opus_decoder.Receive(ADSP::Direction::Host);
if (msg != ADSP::OpusDecoder::Message::DecodeInterleavedForMultiStreamOK) {
LOG_ERROR(Service_Audio, "OpusDecoder returned invalid message. Expected {} got {}",
ADSP::OpusDecoder::Message::DecodeInterleavedForMultiStreamOK, msg);
R_THROW(ResultInvalidOpusDSPReturnCode);
}
auto error_code{static_cast<s32>(shared_memory.dsp_return_data[0])};
if (error_code == OPUS_OK) {
out_sample_count = static_cast<u32>(shared_memory.dsp_return_data[1]);
out_time_taken = 1000 * shared_memory.dsp_return_data[2];
}
R_RETURN(ResultCodeFromLibOpusErrorCode(error_code));
}
Result HardwareOpus::MapMemory(void* buffer, u64 buffer_size) {
std::scoped_lock l{mutex};
shared_memory.host_send_data[0] = (u64)buffer;
shared_memory.host_send_data[1] = buffer_size;
opus_decoder.Send(ADSP::Direction::DSP, ADSP::OpusDecoder::Message::MapMemory);
auto msg = opus_decoder.Receive(ADSP::Direction::Host);
if (msg != ADSP::OpusDecoder::Message::MapMemoryOK) {
LOG_ERROR(Service_Audio, "OpusDecoder returned invalid message. Expected {} got {}",
ADSP::OpusDecoder::Message::MapMemoryOK, msg);
R_THROW(ResultInvalidOpusDSPReturnCode);
}
R_SUCCEED();
}
Result HardwareOpus::UnmapMemory(void* buffer, u64 buffer_size) {
std::scoped_lock l{mutex};
shared_memory.host_send_data[0] = (u64)buffer;
shared_memory.host_send_data[1] = buffer_size;
opus_decoder.Send(ADSP::Direction::DSP, ADSP::OpusDecoder::Message::UnmapMemory);
auto msg = opus_decoder.Receive(ADSP::Direction::Host);
if (msg != ADSP::OpusDecoder::Message::UnmapMemoryOK) {
LOG_ERROR(Service_Audio, "OpusDecoder returned invalid message. Expected {} got {}",
ADSP::OpusDecoder::Message::UnmapMemoryOK, msg);
R_THROW(ResultInvalidOpusDSPReturnCode);
}
R_SUCCEED();
}
} // namespace AudioCore::OpusDecoder

View File

@@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <mutex>
#include <opus.h>
#include "audio_core/adsp/apps/opus/opus_decoder.h"
#include "audio_core/adsp/apps/opus/shared_memory.h"
#include "audio_core/adsp/mailbox.h"
#include "core/hle/service/audio/errors.h"
namespace AudioCore::OpusDecoder {
class HardwareOpus {
public:
HardwareOpus(Core::System& system);
u32 GetWorkBufferSize(u32 channel);
u32 GetWorkBufferSizeForMultiStream(u32 total_stream_count, u32 stereo_stream_count);
Result InitializeDecodeObject(u32 sample_rate, u32 channel_count, void* buffer,
u64 buffer_size);
Result InitializeMultiStreamDecodeObject(u32 sample_rate, u32 channel_count,
u32 totaL_stream_count, u32 stereo_stream_count,
const void* mappings, void* buffer, u64 buffer_size);
Result ShutdownDecodeObject(void* buffer, u64 buffer_size);
Result ShutdownMultiStreamDecodeObject(void* buffer, u64 buffer_size);
Result DecodeInterleaved(u32& out_sample_count, void* output_data, u64 output_data_size,
u32 channel_count, void* input_data, u64 input_data_size, void* buffer,
u64& out_time_taken, bool reset);
Result DecodeInterleavedForMultiStream(u32& out_sample_count, void* output_data,
u64 output_data_size, u32 channel_count,
void* input_data, u64 input_data_size, void* buffer,
u64& out_time_taken, bool reset);
Result MapMemory(void* buffer, u64 buffer_size);
Result UnmapMemory(void* buffer, u64 buffer_size);
private:
Core::System& system;
std::mutex mutex;
ADSP::OpusDecoder::OpusDecoder& opus_decoder;
ADSP::OpusDecoder::SharedMemory shared_memory;
};
} // namespace AudioCore::OpusDecoder

View File

@@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/common_funcs.h"
#include "common/common_types.h"
namespace AudioCore::OpusDecoder {
constexpr size_t OpusStreamCountMax = 255;
constexpr size_t MaxChannels = 2;
struct OpusParameters {
/* 0x00 */ u32 sample_rate;
/* 0x04 */ u32 channel_count;
}; // size = 0x8
static_assert(sizeof(OpusParameters) == 0x8, "OpusParameters has the wrong size!");
struct OpusParametersEx {
/* 0x00 */ u32 sample_rate;
/* 0x04 */ u32 channel_count;
/* 0x08 */ bool use_large_frame_size;
/* 0x09 */ INSERT_PADDING_BYTES_NOINIT(7);
}; // size = 0x10
static_assert(sizeof(OpusParametersEx) == 0x10, "OpusParametersEx has the wrong size!");
struct OpusMultiStreamParameters {
/* 0x00 */ u32 sample_rate;
/* 0x04 */ u32 channel_count;
/* 0x08 */ u32 total_stream_count;
/* 0x0C */ u32 stereo_stream_count;
/* 0x10 */ std::array<u8, OpusStreamCountMax + 1> mappings;
}; // size = 0x110
static_assert(sizeof(OpusMultiStreamParameters) == 0x110,
"OpusMultiStreamParameters has the wrong size!");
struct OpusMultiStreamParametersEx {
/* 0x00 */ u32 sample_rate;
/* 0x04 */ u32 channel_count;
/* 0x08 */ u32 total_stream_count;
/* 0x0C */ u32 stereo_stream_count;
/* 0x10 */ bool use_large_frame_size;
/* 0x11 */ INSERT_PADDING_BYTES_NOINIT(7);
/* 0x18 */ std::array<u8, OpusStreamCountMax + 1> mappings;
}; // size = 0x118
static_assert(sizeof(OpusMultiStreamParametersEx) == 0x118,
"OpusMultiStreamParametersEx has the wrong size!");
struct OpusPacketHeader {
/* 0x00 */ u32 size;
/* 0x04 */ u32 final_range;
}; // size = 0x8
static_assert(sizeof(OpusPacketHeader) == 0x8, "OpusPacketHeader has the wrong size!");
} // namespace AudioCore::OpusDecoder

100
src/audio_core/out/audio_out.cpp Executable file
View File

@@ -0,0 +1,100 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/audio_out_manager.h"
#include "audio_core/out/audio_out.h"
#include "core/hle/kernel/k_event.h"
namespace AudioCore::AudioOut {
Out::Out(Core::System& system_, Manager& manager_, Kernel::KEvent* event_, size_t session_id_)
: manager{manager_}, parent_mutex{manager.mutex}, event{event_}, system{system_, event,
session_id_} {}
void Out::Free() {
std::scoped_lock l{parent_mutex};
manager.ReleaseSessionId(system.GetSessionId());
}
System& Out::GetSystem() {
return system;
}
AudioOut::State Out::GetState() {
std::scoped_lock l{parent_mutex};
return system.GetState();
}
Result Out::StartSystem() {
std::scoped_lock l{parent_mutex};
return system.Start();
}
void Out::StartSession() {
std::scoped_lock l{parent_mutex};
system.StartSession();
}
Result Out::StopSystem() {
std::scoped_lock l{parent_mutex};
return system.Stop();
}
Result Out::AppendBuffer(const AudioOutBuffer& buffer, const u64 tag) {
std::scoped_lock l{parent_mutex};
if (system.AppendBuffer(buffer, tag)) {
return ResultSuccess;
}
return Service::Audio::ResultBufferCountReached;
}
void Out::ReleaseAndRegisterBuffers() {
std::scoped_lock l{parent_mutex};
if (system.GetState() == State::Started) {
system.ReleaseBuffers();
system.RegisterBuffers();
}
}
bool Out::FlushAudioOutBuffers() {
std::scoped_lock l{parent_mutex};
return system.FlushAudioOutBuffers();
}
u32 Out::GetReleasedBuffers(std::span<u64> tags) {
std::scoped_lock l{parent_mutex};
return system.GetReleasedBuffers(tags);
}
Kernel::KReadableEvent& Out::GetBufferEvent() {
std::scoped_lock l{parent_mutex};
return event->GetReadableEvent();
}
f32 Out::GetVolume() const {
std::scoped_lock l{parent_mutex};
return system.GetVolume();
}
void Out::SetVolume(const f32 volume) {
std::scoped_lock l{parent_mutex};
system.SetVolume(volume);
}
bool Out::ContainsAudioBuffer(const u64 tag) const {
std::scoped_lock l{parent_mutex};
return system.ContainsAudioBuffer(tag);
}
u32 Out::GetBufferCount() const {
std::scoped_lock l{parent_mutex};
return system.GetBufferCount();
}
u64 Out::GetPlayedSampleCount() const {
std::scoped_lock l{parent_mutex};
return system.GetPlayedSampleCount();
}
} // namespace AudioCore::AudioOut

147
src/audio_core/out/audio_out.h Executable file
View File

@@ -0,0 +1,147 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <mutex>
#include "audio_core/out/audio_out_system.h"
namespace Core {
class System;
}
namespace Kernel {
class KEvent;
class KReadableEvent;
} // namespace Kernel
namespace AudioCore::AudioOut {
class Manager;
/**
* Interface between the service and audio out system. Mainly responsible for forwarding service
* calls to the system.
*/
class Out {
public:
explicit Out(Core::System& system, Manager& manager, Kernel::KEvent* event, size_t session_id);
/**
* Free this audio out from the audio out manager.
*/
void Free();
/**
* Get this audio out's system.
*/
System& GetSystem();
/**
* Get the current state.
*
* @return Started or Stopped.
*/
AudioOut::State GetState();
/**
* Start the system
*
* @return Result code
*/
Result StartSystem();
/**
* Start the system's device session.
*/
void StartSession();
/**
* Stop the system.
*
* @return Result code
*/
Result StopSystem();
/**
* Append a new buffer to the system, the buffer event will be signalled when it is filled.
*
* @param buffer - The new buffer to append.
* @param tag - Unique tag for this buffer.
* @return Result code.
*/
Result AppendBuffer(const AudioOutBuffer& buffer, u64 tag);
/**
* Release all completed buffers, and register any appended.
*/
void ReleaseAndRegisterBuffers();
/**
* Flush all buffers.
*/
bool FlushAudioOutBuffers();
/**
* Get all of the currently released buffers.
*
* @param tags - Output container for the buffer tags which were released.
* @return The number of buffers released.
*/
u32 GetReleasedBuffers(std::span<u64> tags);
/**
* Get the buffer event for this audio out, this event will be signalled when a buffer is
* filled.
* @return The buffer event.
*/
Kernel::KReadableEvent& GetBufferEvent();
/**
* Get the current system volume.
*
* @return The current volume.
*/
f32 GetVolume() const;
/**
* Set the system volume.
*
* @param volume - The volume to set.
*/
void SetVolume(f32 volume);
/**
* Check if a buffer is in the system.
*
* @param tag - The tag to search for.
* @return True if the buffer is in the system, otherwise false.
*/
bool ContainsAudioBuffer(u64 tag) const;
/**
* Get the maximum number of buffers.
*
* @return The maximum number of buffers.
*/
u32 GetBufferCount() const;
/**
* Get the total played sample count for this audio out.
*
* @return The played sample count.
*/
u64 GetPlayedSampleCount() const;
private:
/// The AudioOut::Manager this audio out is registered with
Manager& manager;
/// Manager's mutex
std::recursive_mutex& parent_mutex;
/// Buffer event, signalled when buffers are ready to be released
Kernel::KEvent* event;
/// Main audio out system
System system;
};
} // namespace AudioCore::AudioOut

View File

@@ -0,0 +1,219 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <mutex>
#include "audio_core/audio_event.h"
#include "audio_core/audio_manager.h"
#include "audio_core/out/audio_out_system.h"
#include "common/logging/log.h"
#include "core/core.h"
#include "core/core_timing.h"
#include "core/hle/kernel/k_event.h"
namespace AudioCore::AudioOut {
System::System(Core::System& system_, Kernel::KEvent* event_, size_t session_id_)
: system{system_}, buffer_event{event_},
session_id{session_id_}, session{std::make_unique<DeviceSession>(system_)} {}
System::~System() {
Finalize();
}
void System::Finalize() {
Stop();
session->Finalize();
}
std::string_view System::GetDefaultOutputDeviceName() const {
return "DeviceOut";
}
Result System::IsConfigValid(std::string_view device_name,
const AudioOutParameter& in_params) const {
if ((device_name.size() > 0) && (device_name != GetDefaultOutputDeviceName())) {
return Service::Audio::ResultNotFound;
}
if (in_params.sample_rate != TargetSampleRate && in_params.sample_rate > 0) {
return Service::Audio::ResultInvalidSampleRate;
}
if (in_params.channel_count == 0 || in_params.channel_count == 2 ||
in_params.channel_count == 6) {
return ResultSuccess;
}
return Service::Audio::ResultInvalidChannelCount;
}
Result System::Initialize(std::string device_name, const AudioOutParameter& in_params,
Kernel::KProcess* handle_, u64 applet_resource_user_id_) {
auto result = IsConfigValid(device_name, in_params);
if (result.IsError()) {
return result;
}
handle = handle_;
applet_resource_user_id = applet_resource_user_id_;
if (device_name.empty() || device_name[0] == '\0') {
name = std::string(GetDefaultOutputDeviceName());
} else {
name = std::move(device_name);
}
sample_rate = TargetSampleRate;
sample_format = SampleFormat::PcmInt16;
channel_count = in_params.channel_count <= 2 ? 2 : 6;
volume = 1.0f;
return ResultSuccess;
}
void System::StartSession() {
session->Start();
}
size_t System::GetSessionId() const {
return session_id;
}
Result System::Start() {
if (state != State::Stopped) {
return Service::Audio::ResultOperationFailed;
}
session->Initialize(name, sample_format, channel_count, session_id, handle,
applet_resource_user_id, Sink::StreamType::Out);
session->SetVolume(volume);
session->Start();
state = State::Started;
boost::container::static_vector<AudioBuffer, BufferCount> buffers_to_flush{};
buffers.RegisterBuffers(buffers_to_flush);
session->AppendBuffers(buffers_to_flush);
session->SetRingSize(static_cast<u32>(buffers_to_flush.size()));
return ResultSuccess;
}
Result System::Stop() {
if (state == State::Started) {
session->Stop();
session->SetVolume(0.0f);
session->ClearBuffers();
if (buffers.ReleaseBuffers(system.CoreTiming(), *session, true)) {
buffer_event->Signal();
}
state = State::Stopped;
}
return ResultSuccess;
}
bool System::AppendBuffer(const AudioOutBuffer& buffer, u64 tag) {
if (buffers.GetTotalBufferCount() == BufferCount) {
return false;
}
const auto timestamp{buffers.GetNextTimestamp()};
const AudioBuffer new_buffer{
.start_timestamp = timestamp,
.end_timestamp = timestamp + buffer.size / (channel_count * sizeof(s16)),
.played_timestamp = 0,
.samples = buffer.samples,
.tag = tag,
.size = buffer.size,
};
buffers.AppendBuffer(new_buffer);
RegisterBuffers();
return true;
}
void System::RegisterBuffers() {
if (state == State::Started) {
boost::container::static_vector<AudioBuffer, BufferCount> registered_buffers{};
buffers.RegisterBuffers(registered_buffers);
session->AppendBuffers(registered_buffers);
}
}
void System::ReleaseBuffers() {
bool signal{buffers.ReleaseBuffers(system.CoreTiming(), *session, false)};
if (signal) {
// Signal if any buffer was released, or if none are registered, we need more.
buffer_event->Signal();
}
}
u32 System::GetReleasedBuffers(std::span<u64> tags) {
return buffers.GetReleasedBuffers(tags);
}
bool System::FlushAudioOutBuffers() {
if (state != State::Started) {
return false;
}
u32 buffers_released{};
buffers.FlushBuffers(buffers_released);
if (buffers_released > 0) {
buffer_event->Signal();
}
return true;
}
u16 System::GetChannelCount() const {
return channel_count;
}
u32 System::GetSampleRate() const {
return sample_rate;
}
SampleFormat System::GetSampleFormat() const {
return sample_format;
}
State System::GetState() {
switch (state) {
case State::Started:
case State::Stopped:
return state;
default:
LOG_ERROR(Service_Audio, "AudioOut invalid state!");
state = State::Stopped;
break;
}
return state;
}
std::string System::GetName() const {
return name;
}
f32 System::GetVolume() const {
return volume;
}
void System::SetVolume(const f32 volume_) {
volume = volume_;
session->SetVolume(volume_);
}
bool System::ContainsAudioBuffer(const u64 tag) const {
return buffers.ContainsBuffer(tag);
}
u32 System::GetBufferCount() const {
return buffers.GetAppendedRegisteredCount();
}
u64 System::GetPlayedSampleCount() const {
return session->GetPlayedSampleCount();
}
} // namespace AudioCore::AudioOut

View File

@@ -0,0 +1,258 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <memory>
#include <span>
#include <string>
#include "audio_core/common/common.h"
#include "audio_core/device/audio_buffers.h"
#include "audio_core/device/device_session.h"
#include "core/hle/service/audio/errors.h"
namespace Core {
class System;
}
namespace Kernel {
class KEvent;
class KProcess;
} // namespace Kernel
namespace AudioCore::AudioOut {
constexpr SessionTypes SessionType = SessionTypes::AudioOut;
struct AudioOutParameter {
/* 0x0 */ s32_le sample_rate;
/* 0x4 */ u16_le channel_count;
/* 0x6 */ u16_le reserved;
};
static_assert(sizeof(AudioOutParameter) == 0x8, "AudioOutParameter is an invalid size");
struct AudioOutParameterInternal {
/* 0x0 */ u32_le sample_rate;
/* 0x4 */ u32_le channel_count;
/* 0x8 */ u32_le sample_format;
/* 0xC */ u32_le state;
};
static_assert(sizeof(AudioOutParameterInternal) == 0x10,
"AudioOutParameterInternal is an invalid size");
struct AudioOutBuffer {
/* 0x00 */ AudioOutBuffer* next;
/* 0x08 */ VAddr samples;
/* 0x10 */ u64 capacity;
/* 0x18 */ u64 size;
/* 0x20 */ u64 offset;
};
static_assert(sizeof(AudioOutBuffer) == 0x28, "AudioOutBuffer is an invalid size");
enum class State {
Started,
Stopped,
};
/**
* Controls and drives audio output.
*/
class System {
public:
explicit System(Core::System& system, Kernel::KEvent* event, size_t session_id);
~System();
/**
* Get the default audio output device name.
*
* @return The default audio output device name.
*/
std::string_view GetDefaultOutputDeviceName() const;
/**
* Is the given initialize config valid?
*
* @param device_name - The name of the requested output device.
* @param in_params - Input parameters, see AudioOutParameter.
* @return Result code.
*/
Result IsConfigValid(std::string_view device_name, const AudioOutParameter& in_params) const;
/**
* Initialize this system.
*
* @param device_name - The name of the requested output device.
* @param in_params - Input parameters, see AudioOutParameter.
* @param handle - Process handle.
* @param applet_resource_user_id - Unused.
* @return Result code.
*/
Result Initialize(std::string device_name, const AudioOutParameter& in_params,
Kernel::KProcess* handle, u64 applet_resource_user_id);
/**
* Start this system.
*
* @return Result code.
*/
Result Start();
/**
* Stop this system.
*
* @return Result code.
*/
Result Stop();
/**
* Finalize this system.
*/
void Finalize();
/**
* Start this system's device session.
*/
void StartSession();
/**
* Get this system's id.
*/
size_t GetSessionId() const;
/**
* Append a new buffer to the device.
*
* @param buffer - New buffer to append.
* @param tag - Unique tag of the buffer.
* @return True if the buffer was appended, otherwise false.
*/
bool AppendBuffer(const AudioOutBuffer& buffer, u64 tag);
/**
* Register all appended buffers.
*/
void RegisterBuffers();
/**
* Release all registered buffers.
*/
void ReleaseBuffers();
/**
* Get all released buffers.
*
* @param tags - Container to be filled with the released buffers' tags.
* @return The number of buffers released.
*/
u32 GetReleasedBuffers(std::span<u64> tags);
/**
* Flush all appended and registered buffers.
*
* @return True if buffers were successfully flushed, otherwise false.
*/
bool FlushAudioOutBuffers();
/**
* Get this system's current channel count.
*
* @return The channel count.
*/
u16 GetChannelCount() const;
/**
* Get this system's current sample rate.
*
* @return The sample rate.
*/
u32 GetSampleRate() const;
/**
* Get this system's current sample format.
*
* @return The sample format.
*/
SampleFormat GetSampleFormat() const;
/**
* Get this system's current state.
*
* @return The current state.
*/
State GetState();
/**
* Get this system's name.
*
* @return The system's name.
*/
std::string GetName() const;
/**
* Get this system's current volume.
*
* @return The system's current volume.
*/
f32 GetVolume() const;
/**
* Set this system's current volume.
*
* @param volume The new volume.
*/
void SetVolume(f32 volume);
/**
* Does the system contain this buffer?
*
* @param tag - Unique tag to search for.
* @return True if the buffer is in the system, otherwise false.
*/
bool ContainsAudioBuffer(u64 tag) const;
/**
* Get the maximum number of usable buffers (default 32).
*
* @return The number of buffers.
*/
u32 GetBufferCount() const;
/**
* Get the total number of samples played by this system.
*
* @return The number of samples.
*/
u64 GetPlayedSampleCount() const;
private:
/// Core system
Core::System& system;
/// Process handle
Kernel::KProcess* handle{};
/// (Unused)
u64 applet_resource_user_id{};
/// Buffer event, signalled when a buffer is ready
Kernel::KEvent* buffer_event;
/// Session id of this system
size_t session_id{};
/// Device session for this system
std::unique_ptr<DeviceSession> session;
/// Audio buffers in use by this system
AudioBuffers<BufferCount> buffers{BufferCount};
/// Sample rate of this system
u32 sample_rate{};
/// Sample format of this system
SampleFormat sample_format{SampleFormat::PcmInt16};
/// Channel count of this system
u16 channel_count{};
/// State of this system
std::atomic<State> state{State::Stopped};
/// Name of this system
std::string name{};
/// Volume of this system
f32 volume{1.0f};
};
} // namespace AudioCore::AudioOut

View File

@@ -0,0 +1,6 @@
// SPDX-FileCopyrightText: 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "common/common_precompiled_headers.h"

View File

@@ -0,0 +1,117 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/renderer/adsp/adsp.h"
#include "audio_core/renderer/adsp/command_buffer.h"
#include "audio_core/sink/sink.h"
#include "common/logging/log.h"
#include "core/core.h"
#include "core/core_timing.h"
#include "core/memory.h"
namespace AudioCore::AudioRenderer::ADSP {
ADSP::ADSP(Core::System& system_, Sink::Sink& sink_)
: system{system_}, memory{system.ApplicationMemory()}, sink{sink_} {}
ADSP::~ADSP() {
ClearCommandBuffers();
}
State ADSP::GetState() const {
if (running) {
return State::Started;
}
return State::Stopped;
}
AudioRenderer_Mailbox* ADSP::GetRenderMailbox() {
return &render_mailbox;
}
void ADSP::ClearRemainCount(const u32 session_id) {
render_mailbox.ClearRemainCount(session_id);
}
u64 ADSP::GetSignalledTick() const {
return render_mailbox.GetSignalledTick();
}
u64 ADSP::GetTimeTaken() const {
return render_mailbox.GetRenderTimeTaken();
}
u64 ADSP::GetRenderTimeTaken(const u32 session_id) {
return render_mailbox.GetCommandBuffer(session_id).render_time_taken;
}
u32 ADSP::GetRemainCommandCount(const u32 session_id) const {
return render_mailbox.GetRemainCommandCount(session_id);
}
void ADSP::SendCommandBuffer(const u32 session_id, const CommandBuffer& command_buffer) {
render_mailbox.SetCommandBuffer(session_id, command_buffer);
}
u64 ADSP::GetRenderingStartTick(const u32 session_id) {
return render_mailbox.GetSignalledTick() +
render_mailbox.GetCommandBuffer(session_id).render_time_taken;
}
bool ADSP::Start() {
if (running) {
return running;
}
running = true;
systems_active++;
audio_renderer = std::make_unique<AudioRenderer>(system);
audio_renderer->Start(&render_mailbox);
render_mailbox.HostSendMessage(RenderMessage::AudioRenderer_InitializeOK);
if (render_mailbox.HostWaitMessage() != RenderMessage::AudioRenderer_InitializeOK) {
LOG_ERROR(
Service_Audio,
"Host Audio Renderer -- Failed to receive initialize message response from ADSP!");
}
return running;
}
void ADSP::Stop() {
systems_active--;
if (running && systems_active == 0) {
{
std::scoped_lock l{mailbox_lock};
render_mailbox.HostSendMessage(RenderMessage::AudioRenderer_Shutdown);
if (render_mailbox.HostWaitMessage() != RenderMessage::AudioRenderer_Shutdown) {
LOG_ERROR(Service_Audio, "Host Audio Renderer -- Failed to receive shutdown "
"message response from ADSP!");
}
}
audio_renderer->Stop();
running = false;
}
}
void ADSP::Signal() {
const auto signalled_tick{system.CoreTiming().GetClockTicks()};
render_mailbox.SetSignalledTick(signalled_tick);
render_mailbox.HostSendMessage(RenderMessage::AudioRenderer_Render);
}
void ADSP::Wait() {
std::scoped_lock l{mailbox_lock};
auto response{render_mailbox.HostWaitMessage()};
if (response != RenderMessage::AudioRenderer_RenderResponse) {
LOG_ERROR(Service_Audio, "Invalid ADSP response message, expected 0x{:02X}, got 0x{:02X}",
static_cast<u32>(RenderMessage::AudioRenderer_RenderResponse),
static_cast<u32>(response));
}
ClearCommandBuffers();
}
void ADSP::ClearCommandBuffers() {
render_mailbox.ClearCommandBuffers();
}
} // namespace AudioCore::AudioRenderer::ADSP

View File

@@ -0,0 +1,171 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <memory>
#include <mutex>
#include "audio_core/renderer/adsp/audio_renderer.h"
#include "common/common_types.h"
namespace Core {
namespace Memory {
class Memory;
}
class System;
} // namespace Core
namespace AudioCore {
namespace Sink {
class Sink;
}
namespace AudioRenderer::ADSP {
struct CommandBuffer;
enum class State {
Started,
Stopped,
};
/**
* Represents the ADSP embedded within the audio sysmodule.
* This is a 32-bit Linux4Tegra kernel from nVidia, which is launched with the sysmodule on boot.
*
* The kernel will run apps you program for it, Nintendo have the following:
*
* Gmix - Responsible for mixing final audio and sending it out to hardware. This is last place all
* audio samples end up, and we skip it entirely, since we have very different backends and
* mixing is implicitly handled by the OS (but also due to lack of research/simplicity).
*
* AudioRenderer - Receives command lists generated by the audio render
* system, processes them, and sends the samples to Gmix.
*
* OpusDecoder - Contains libopus, and controls processing Opus audio and sends it to Gmix.
* Not much research done here, TODO if needed.
*
* We only implement the AudioRenderer for now.
*
* Communication for the apps is done through mailboxes, and some shared memory.
*/
class ADSP {
public:
explicit ADSP(Core::System& system, Sink::Sink& sink);
~ADSP();
/**
* Start the ADSP.
*
* @return True if started or already running, otherwise false.
*/
bool Start();
/**
* Stop the ADSP.
*/
void Stop();
/**
* Get the ADSP's state.
*
* @return Started or Stopped.
*/
State GetState() const;
/**
* Get the AudioRenderer mailbox to communicate with it.
*
* @return The AudioRenderer mailbox.
*/
AudioRenderer_Mailbox* GetRenderMailbox();
/**
* Get the tick the ADSP was signalled.
*
* @return The tick the ADSP was signalled.
*/
u64 GetSignalledTick() const;
/**
* Get the total time it took for the ADSP to run the last command lists (both command lists).
*
* @return The tick the ADSP was signalled.
*/
u64 GetTimeTaken() const;
/**
* Get the last time a given command list took to run.
*
* @param session_id - The session id to check (0 or 1).
* @return The time it took.
*/
u64 GetRenderTimeTaken(u32 session_id);
/**
* Clear the remaining command count for a given session.
*
* @param session_id - The session id to check (0 or 1).
*/
void ClearRemainCount(u32 session_id);
/**
* Get the remaining number of commands left to process for a command list.
*
* @param session_id - The session id to check (0 or 1).
* @return The number of commands remaining.
*/
u32 GetRemainCommandCount(u32 session_id) const;
/**
* Get the last tick a command list started processing.
*
* @param session_id - The session id to check (0 or 1).
* @return The last tick the given command list started.
*/
u64 GetRenderingStartTick(u32 session_id);
/**
* Set a command buffer to be processed.
*
* @param session_id - The session id to check (0 or 1).
* @param command_buffer - The command buffer to process.
*/
void SendCommandBuffer(u32 session_id, const CommandBuffer& command_buffer);
/**
* Clear the command buffers (does not clear the time taken or the remaining command count)
*/
void ClearCommandBuffers();
/**
* Signal the AudioRenderer to begin processing.
*/
void Signal();
/**
* Wait for the AudioRenderer to finish processing.
*/
void Wait();
private:
/// Core system
Core::System& system;
/// Core memory
Core::Memory::Memory& memory;
/// Number of systems active, used to prevent accidental shutdowns
u8 systems_active{0};
/// ADSP running state
std::atomic<bool> running{false};
/// Output sink used by the ADSP
Sink::Sink& sink;
/// AudioRenderer app
std::unique_ptr<AudioRenderer> audio_renderer{};
/// Communication for the AudioRenderer
AudioRenderer_Mailbox render_mailbox{};
/// Mailbox lock ffor the render mailbox
std::mutex mailbox_lock;
};
} // namespace AudioRenderer::ADSP
} // namespace AudioCore

View File

@@ -0,0 +1,225 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <array>
#include <chrono>
#include "audio_core/audio_core.h"
#include "audio_core/common/common.h"
#include "audio_core/renderer/adsp/audio_renderer.h"
#include "audio_core/sink/sink.h"
#include "common/logging/log.h"
#include "common/microprofile.h"
#include "common/thread.h"
#include "core/core.h"
#include "core/core_timing.h"
MICROPROFILE_DEFINE(Audio_Renderer, "Audio", "DSP", MP_RGB(60, 19, 97));
namespace AudioCore::AudioRenderer::ADSP {
void AudioRenderer_Mailbox::HostSendMessage(RenderMessage message_) {
adsp_messages.enqueue(message_);
adsp_event.Set();
}
RenderMessage AudioRenderer_Mailbox::HostWaitMessage() {
host_event.Wait();
RenderMessage msg{RenderMessage::Invalid};
if (!host_messages.try_dequeue(msg)) {
LOG_ERROR(Service_Audio, "Failed to dequeue host message!");
}
return msg;
}
void AudioRenderer_Mailbox::ADSPSendMessage(const RenderMessage message_) {
host_messages.enqueue(message_);
host_event.Set();
}
RenderMessage AudioRenderer_Mailbox::ADSPWaitMessage() {
adsp_event.Wait();
RenderMessage msg{RenderMessage::Invalid};
if (!adsp_messages.try_dequeue(msg)) {
LOG_ERROR(Service_Audio, "Failed to dequeue ADSP message!");
}
return msg;
}
CommandBuffer& AudioRenderer_Mailbox::GetCommandBuffer(const u32 session_id) {
return command_buffers[session_id];
}
void AudioRenderer_Mailbox::SetCommandBuffer(const u32 session_id, const CommandBuffer& buffer) {
command_buffers[session_id] = buffer;
}
u64 AudioRenderer_Mailbox::GetRenderTimeTaken() const {
return command_buffers[0].render_time_taken + command_buffers[1].render_time_taken;
}
u64 AudioRenderer_Mailbox::GetSignalledTick() const {
return signalled_tick;
}
void AudioRenderer_Mailbox::SetSignalledTick(const u64 tick) {
signalled_tick = tick;
}
void AudioRenderer_Mailbox::ClearRemainCount(const u32 session_id) {
command_buffers[session_id].remaining_command_count = 0;
}
u32 AudioRenderer_Mailbox::GetRemainCommandCount(const u32 session_id) const {
return command_buffers[session_id].remaining_command_count;
}
void AudioRenderer_Mailbox::ClearCommandBuffers() {
command_buffers[0].buffer = 0;
command_buffers[0].size = 0;
command_buffers[0].reset_buffers = false;
command_buffers[1].buffer = 0;
command_buffers[1].size = 0;
command_buffers[1].reset_buffers = false;
}
AudioRenderer::AudioRenderer(Core::System& system_)
: system{system_}, sink{system.AudioCore().GetOutputSink()} {
CreateSinkStreams();
}
AudioRenderer::~AudioRenderer() {
Stop();
for (auto& stream : streams) {
if (stream) {
sink.CloseStream(stream);
}
stream = nullptr;
}
}
void AudioRenderer::Start(AudioRenderer_Mailbox* mailbox_) {
if (running) {
return;
}
mailbox = mailbox_;
thread = std::jthread([this](std::stop_token stop_token) { ThreadFunc(stop_token); });
running = true;
}
void AudioRenderer::Stop() {
if (!running) {
return;
}
for (auto& stream : streams) {
stream->Stop();
}
thread.join();
running = false;
}
void AudioRenderer::CreateSinkStreams() {
u32 channels{sink.GetDeviceChannels()};
for (u32 i = 0; i < MaxRendererSessions; i++) {
std::string name{fmt::format("ADSP_RenderStream-{}", i)};
streams[i] =
sink.AcquireSinkStream(system, channels, name, ::AudioCore::Sink::StreamType::Render);
streams[i]->SetRingSize(4);
}
}
void AudioRenderer::ThreadFunc(std::stop_token stop_token) {
static constexpr char name[]{"AudioRenderer"};
MicroProfileOnThreadCreate(name);
Common::SetCurrentThreadName(name);
Common::SetCurrentThreadPriority(Common::ThreadPriority::High);
if (mailbox->ADSPWaitMessage() != RenderMessage::AudioRenderer_InitializeOK) {
LOG_ERROR(Service_Audio,
"ADSP Audio Renderer -- Failed to receive initialize message from host!");
return;
}
mailbox->ADSPSendMessage(RenderMessage::AudioRenderer_InitializeOK);
// 0.12 seconds (2304000 / 19200000)
constexpr u64 max_process_time{2'304'000ULL};
while (!stop_token.stop_requested()) {
auto message{mailbox->ADSPWaitMessage()};
switch (message) {
case RenderMessage::AudioRenderer_Shutdown:
mailbox->ADSPSendMessage(RenderMessage::AudioRenderer_Shutdown);
return;
case RenderMessage::AudioRenderer_Render: {
if (system.IsShuttingDown()) [[unlikely]] {
std::this_thread::sleep_for(std::chrono::milliseconds(5));
mailbox->ADSPSendMessage(RenderMessage::AudioRenderer_RenderResponse);
continue;
}
std::array<bool, MaxRendererSessions> buffers_reset{};
std::array<u64, MaxRendererSessions> render_times_taken{};
const auto start_time{system.CoreTiming().GetClockTicks()};
for (u32 index = 0; index < 2; index++) {
auto& command_buffer{mailbox->GetCommandBuffer(index)};
auto& command_list_processor{command_list_processors[index]};
// Check this buffer is valid, as it may not be used.
if (command_buffer.buffer != 0) {
// If there are no remaining commands (from the previous list),
// this is a new command list, initialize it.
if (command_buffer.remaining_command_count == 0) {
command_list_processor.Initialize(system, command_buffer.buffer,
command_buffer.size, streams[index]);
}
if (command_buffer.reset_buffers && !buffers_reset[index]) {
streams[index]->ClearQueue();
buffers_reset[index] = true;
}
u64 max_time{max_process_time};
if (index == 1 && command_buffer.applet_resource_user_id ==
mailbox->GetCommandBuffer(0).applet_resource_user_id) {
max_time = max_process_time - render_times_taken[0];
if (render_times_taken[0] > max_process_time) {
max_time = 0;
}
}
max_time = std::min(command_buffer.time_limit, max_time);
command_list_processor.SetProcessTimeMax(max_time);
streams[index]->WaitFreeSpace(stop_token);
// Process the command list
{
MICROPROFILE_SCOPE(Audio_Renderer);
render_times_taken[index] =
command_list_processor.Process(index) - start_time;
}
const auto end_time{system.CoreTiming().GetClockTicks()};
command_buffer.remaining_command_count =
command_list_processor.GetRemainingCommandCount();
command_buffer.render_time_taken = end_time - start_time;
}
}
mailbox->ADSPSendMessage(RenderMessage::AudioRenderer_RenderResponse);
} break;
default:
LOG_WARNING(Service_Audio,
"ADSP AudioRenderer received an invalid message, msg={:02X}!",
static_cast<u32>(message));
break;
}
}
}
} // namespace AudioCore::AudioRenderer::ADSP

View File

@@ -0,0 +1,204 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <memory>
#include <thread>
#include "audio_core/renderer/adsp/command_buffer.h"
#include "audio_core/renderer/adsp/command_list_processor.h"
#include "common/common_types.h"
#include "common/polyfill_thread.h"
#include "common/reader_writer_queue.h"
#include "common/thread.h"
namespace Core {
namespace Timing {
struct EventType;
}
class System;
} // namespace Core
namespace AudioCore {
namespace Sink {
class Sink;
}
namespace AudioRenderer::ADSP {
enum class RenderMessage {
/* 0x00 */ Invalid,
/* 0x01 */ AudioRenderer_MapUnmap_Map,
/* 0x02 */ AudioRenderer_MapUnmap_MapResponse,
/* 0x03 */ AudioRenderer_MapUnmap_Unmap,
/* 0x04 */ AudioRenderer_MapUnmap_UnmapResponse,
/* 0x05 */ AudioRenderer_MapUnmap_InvalidateCache,
/* 0x06 */ AudioRenderer_MapUnmap_InvalidateCacheResponse,
/* 0x07 */ AudioRenderer_MapUnmap_Shutdown,
/* 0x08 */ AudioRenderer_MapUnmap_ShutdownResponse,
/* 0x16 */ AudioRenderer_InitializeOK = 0x16,
/* 0x20 */ AudioRenderer_RenderResponse = 0x20,
/* 0x2A */ AudioRenderer_Render = 0x2A,
/* 0x34 */ AudioRenderer_Shutdown = 0x34,
};
/**
* A mailbox for the AudioRenderer, allowing communication between the host and the AudioRenderer
* running on the ADSP.
*/
class AudioRenderer_Mailbox {
public:
/**
* Send a message from the host to the AudioRenderer.
*
* @param message - The message to send to the AudioRenderer.
*/
void HostSendMessage(RenderMessage message);
/**
* Host wait for a message from the AudioRenderer.
*
* @return The message returned from the AudioRenderer.
*/
RenderMessage HostWaitMessage();
/**
* Send a message from the AudioRenderer to the host.
*
* @param message - The message to send to the host.
*/
void ADSPSendMessage(RenderMessage message);
/**
* AudioRenderer wait for a message from the host.
*
* @return The message returned from the AudioRenderer.
*/
RenderMessage ADSPWaitMessage();
/**
* Get the command buffer with the given session id (0 or 1).
*
* @param session_id - The session id to get (0 or 1).
* @return The command buffer.
*/
CommandBuffer& GetCommandBuffer(u32 session_id);
/**
* Set the command buffer with the given session id (0 or 1).
*
* @param session_id - The session id to get (0 or 1).
* @param buffer - The command buffer to set.
*/
void SetCommandBuffer(u32 session_id, const CommandBuffer& buffer);
/**
* Get the total render time taken for the last command lists sent.
*
* @return Total render time taken for the last command lists.
*/
u64 GetRenderTimeTaken() const;
/**
* Get the tick the AudioRenderer was signalled.
*
* @return The tick the AudioRenderer was signalled.
*/
u64 GetSignalledTick() const;
/**
* Set the tick the AudioRenderer was signalled.
*
* @param tick - The tick the AudioRenderer was signalled.
*/
void SetSignalledTick(u64 tick);
/**
* Clear the remaining command count.
*
* @param session_id - Index for which command list to clear (0 or 1).
*/
void ClearRemainCount(u32 session_id);
/**
* Get the remaining command count for a given command list.
*
* @param session_id - Index for which command list to clear (0 or 1).
* @return The remaining command count.
*/
u32 GetRemainCommandCount(u32 session_id) const;
/**
* Clear the command buffers (does not clear the time taken or the remaining command count).
*/
void ClearCommandBuffers();
private:
/// Host signalling event
Common::Event host_event{};
/// AudioRenderer signalling event
Common::Event adsp_event{};
/// Host message queue
Common::ReaderWriterQueue<RenderMessage> host_messages{};
/// AudioRenderer message queue
Common::ReaderWriterQueue<RenderMessage> adsp_messages{};
/// Command buffers
std::array<CommandBuffer, MaxRendererSessions> command_buffers{};
/// Tick the AudioRnederer was signalled
u64 signalled_tick{};
};
/**
* The AudioRenderer application running on the ADSP.
*/
class AudioRenderer {
public:
explicit AudioRenderer(Core::System& system);
~AudioRenderer();
/**
* Start the AudioRenderer.
*
* @param mailbox The mailbox to use for this session.
*/
void Start(AudioRenderer_Mailbox* mailbox);
/**
* Stop the AudioRenderer.
*/
void Stop();
private:
/**
* Main AudioRenderer thread, responsible for processing the command lists.
*/
void ThreadFunc(std::stop_token stop_token);
/**
* Creates the streams which will receive the processed samples.
*/
void CreateSinkStreams();
/// Core system
Core::System& system;
/// Main thread
std::jthread thread{};
/// The current state
std::atomic<bool> running{};
/// The active mailbox
AudioRenderer_Mailbox* mailbox{};
/// The command lists to process
std::array<CommandListProcessor, MaxRendererSessions> command_list_processors{};
/// The output sink the AudioRenderer will use
Sink::Sink& sink;
/// The streams which will receive the processed samples
std::array<Sink::SinkStream*, MaxRendererSessions> streams;
};
} // namespace AudioRenderer::ADSP
} // namespace AudioCore

View File

@@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "audio_core/common/common.h"
#include "common/common_types.h"
namespace AudioCore::AudioRenderer::ADSP {
struct CommandBuffer {
CpuAddr buffer;
u64 size;
u64 time_limit;
u32 remaining_command_count;
bool reset_buffers;
u64 applet_resource_user_id;
u64 render_time_taken;
};
} // namespace AudioCore::AudioRenderer::ADSP

View File

@@ -0,0 +1,108 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <string>
#include "audio_core/renderer/adsp/command_list_processor.h"
#include "audio_core/renderer/command/command_list_header.h"
#include "audio_core/renderer/command/commands.h"
#include "common/settings.h"
#include "core/core.h"
#include "core/core_timing.h"
#include "core/memory.h"
namespace AudioCore::AudioRenderer::ADSP {
void CommandListProcessor::Initialize(Core::System& system_, CpuAddr buffer, u64 size,
Sink::SinkStream* stream_) {
system = &system_;
memory = &system->ApplicationMemory();
stream = stream_;
header = reinterpret_cast<CommandListHeader*>(buffer);
commands = reinterpret_cast<u8*>(buffer + sizeof(CommandListHeader));
commands_buffer_size = size;
command_count = header->command_count;
sample_count = header->sample_count;
target_sample_rate = header->sample_rate;
mix_buffers = header->samples_buffer;
buffer_count = header->buffer_count;
processed_command_count = 0;
}
void CommandListProcessor::SetProcessTimeMax(const u64 time) {
max_process_time = time;
}
u32 CommandListProcessor::GetRemainingCommandCount() const {
return command_count - processed_command_count;
}
void CommandListProcessor::SetBuffer(const CpuAddr buffer, const u64 size) {
commands = reinterpret_cast<u8*>(buffer + sizeof(CommandListHeader));
commands_buffer_size = size;
}
Sink::SinkStream* CommandListProcessor::GetOutputSinkStream() const {
return stream;
}
u64 CommandListProcessor::Process(u32 session_id) {
const auto start_time_{system->CoreTiming().GetClockTicks()};
const auto command_base{CpuAddr(commands)};
if (processed_command_count > 0) {
current_processing_time += start_time_ - end_time;
} else {
start_time = start_time_;
current_processing_time = 0;
}
std::string dump{fmt::format("\nSession {}\n", session_id)};
for (u32 index = 0; index < command_count; index++) {
auto& command{*reinterpret_cast<ICommand*>(commands)};
if (command.magic != 0xCAFEBABE) {
LOG_ERROR(Service_Audio, "Command has invalid magic! Expected 0xCAFEBABE, got {:08X}",
command.magic);
return system->CoreTiming().GetClockTicks() - start_time_;
}
auto current_offset{CpuAddr(commands) - command_base};
if (current_offset + command.size > commands_buffer_size) {
LOG_ERROR(Service_Audio,
"Command exceeded command buffer, buffer size {:08X}, command ends at {:08X}",
commands_buffer_size,
CpuAddr(commands) + command.size - sizeof(CommandListHeader));
return system->CoreTiming().GetClockTicks() - start_time_;
}
if (Settings::values.dump_audio_commands) {
command.Dump(*this, dump);
}
if (!command.Verify(*this)) {
break;
}
if (command.enabled) {
command.Process(*this);
} else {
dump += fmt::format("\tDisabled!\n");
}
processed_command_count++;
commands += command.size;
}
if (Settings::values.dump_audio_commands && dump != last_dump) {
LOG_WARNING(Service_Audio, "{}", dump);
last_dump = dump;
}
end_time = system->CoreTiming().GetClockTicks();
return end_time - start_time_;
}
} // namespace AudioCore::AudioRenderer::ADSP

View File

@@ -0,0 +1,119 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <span>
#include "audio_core/common/common.h"
#include "common/common_types.h"
namespace Core {
namespace Memory {
class Memory;
}
class System;
} // namespace Core
namespace AudioCore {
namespace Sink {
class SinkStream;
}
namespace AudioRenderer {
struct CommandListHeader;
namespace ADSP {
/**
* A processor for command lists given to the AudioRenderer.
*/
class CommandListProcessor {
public:
/**
* Initialize the processor.
*
* @param system - The core system.
* @param buffer - The command buffer to process.
* @param size - The size of the buffer.
* @param stream - The stream to be used for sending the samples.
*/
void Initialize(Core::System& system, CpuAddr buffer, u64 size, Sink::SinkStream* stream);
/**
* Set the maximum processing time for this command list.
*
* @param time - The maximum process time.
*/
void SetProcessTimeMax(u64 time);
/**
* Get the remaining command count for this list.
*
* @return The remaining command count.
*/
u32 GetRemainingCommandCount() const;
/**
* Set the command buffer.
*
* @param buffer - The buffer to use.
* @param size - The size of the buffer.
*/
void SetBuffer(CpuAddr buffer, u64 size);
/**
* Get the stream for this command list.
*
* @return The stream associated with this command list.
*/
Sink::SinkStream* GetOutputSinkStream() const;
/**
* Process the command list.
*
* @param session_id - Session ID for the commands being processed.
*
* @return The time taken to process.
*/
u64 Process(u32 session_id);
/// Core system
Core::System* system{};
/// Core memory
Core::Memory::Memory* memory{};
/// Stream for the processed samples
Sink::SinkStream* stream{};
/// Header info for this command list
CommandListHeader* header{};
/// The command buffer
u8* commands{};
/// The command buffer size
u64 commands_buffer_size{};
/// The maximum processing time allotted
u64 max_process_time{};
/// The number of commands in the buffer
u32 command_count{};
/// The target sample count for output
u32 sample_count{};
/// The target sample rate for output
u32 target_sample_rate{};
/// The mixing buffers used by the commands
std::span<s32> mix_buffers{};
/// The number of mix buffers
u32 buffer_count{};
/// The number of processed commands so far
u32 processed_command_count{};
/// The processing start time of this list
u64 start_time{};
/// The current processing time for this list
u64 current_processing_time{};
/// The end processing time for this list
u64 end_time{};
/// Last command list string generated, used for dumping audio commands to console
std::string last_dump{};
};
} // namespace ADSP
} // namespace AudioRenderer
} // namespace AudioCore

View File

@@ -0,0 +1,72 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <array>
#include <span>
#include "audio_core/audio_core.h"
#include "audio_core/common/feature_support.h"
#include "audio_core/renderer/audio_device.h"
#include "audio_core/sink/sink.h"
#include "core/core.h"
namespace AudioCore::Renderer {
constexpr std::array usb_device_names{
AudioDevice::AudioDeviceName{"AudioStereoJackOutput"},
AudioDevice::AudioDeviceName{"AudioBuiltInSpeakerOutput"},
AudioDevice::AudioDeviceName{"AudioTvOutput"},
AudioDevice::AudioDeviceName{"AudioUsbDeviceOutput"},
};
constexpr std::array device_names{
AudioDevice::AudioDeviceName{"AudioStereoJackOutput"},
AudioDevice::AudioDeviceName{"AudioBuiltInSpeakerOutput"},
AudioDevice::AudioDeviceName{"AudioTvOutput"},
};
constexpr std::array output_device_names{
AudioDevice::AudioDeviceName{"AudioBuiltInSpeakerOutput"},
AudioDevice::AudioDeviceName{"AudioTvOutput"},
AudioDevice::AudioDeviceName{"AudioExternalOutput"},
};
AudioDevice::AudioDevice(Core::System& system, const u64 applet_resource_user_id_,
const u32 revision)
: output_sink{system.AudioCore().GetOutputSink()},
applet_resource_user_id{applet_resource_user_id_}, user_revision{revision} {}
u32 AudioDevice::ListAudioDeviceName(std::span<AudioDeviceName> out_buffer) const {
std::span<const AudioDeviceName> names{};
if (CheckFeatureSupported(SupportTags::AudioUsbDeviceOutput, user_revision)) {
names = usb_device_names;
} else {
names = device_names;
}
const u32 out_count{static_cast<u32>(std::min(out_buffer.size(), names.size()))};
for (u32 i = 0; i < out_count; i++) {
out_buffer[i] = names[i];
}
return out_count;
}
u32 AudioDevice::ListAudioOutputDeviceName(std::span<AudioDeviceName> out_buffer) const {
const u32 out_count{static_cast<u32>(std::min(out_buffer.size(), output_device_names.size()))};
for (u32 i = 0; i < out_count; i++) {
out_buffer[i] = output_device_names[i];
}
return out_count;
}
void AudioDevice::SetDeviceVolumes(const f32 volume) {
output_sink.SetDeviceVolume(volume);
}
f32 AudioDevice::GetDeviceVolume([[maybe_unused]] std::string_view name) const {
return output_sink.GetDeviceVolume();
}
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,78 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <string_view>
#include "audio_core/audio_render_manager.h"
namespace Core {
class System;
}
namespace AudioCore {
namespace Sink {
class Sink;
}
namespace Renderer {
/**
* An interface to an output audio device available to the Switch.
*/
class AudioDevice {
public:
struct AudioDeviceName {
std::array<char, 0x100> name{};
constexpr AudioDeviceName(std::string_view name_) {
name_.copy(name.data(), name.size() - 1);
}
};
explicit AudioDevice(Core::System& system, u64 applet_resource_user_id, u32 revision);
/**
* Get a list of the available output devices.
*
* @param out_buffer - Output buffer to write the available device names.
* @return Number of device names written.
*/
u32 ListAudioDeviceName(std::span<AudioDeviceName> out_buffer) const;
/**
* Get a list of the available output devices.
* Different to above somehow...
*
* @param out_buffer - Output buffer to write the available device names.
* @return Number of device names written.
*/
u32 ListAudioOutputDeviceName(std::span<AudioDeviceName> out_buffer) const;
/**
* Set the volume of all streams in the backend sink.
*
* @param volume - Volume to set.
*/
void SetDeviceVolumes(f32 volume);
/**
* Get the volume for a given device name.
* Note: This is not fully implemented, we only assume 1 device for all streams.
*
* @param name - Name of the device to check. Unused.
* @return Volume of the device.
*/
f32 GetDeviceVolume(std::string_view name) const;
private:
/// Backend output sink for the device
Sink::Sink& output_sink;
/// Resource id this device is used for
const u64 applet_resource_user_id;
/// User audio renderer revision
const u32 user_revision;
};
} // namespace Renderer
} // namespace AudioCore

View File

@@ -0,0 +1,68 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/audio_render_manager.h"
#include "audio_core/common/audio_renderer_parameter.h"
#include "audio_core/renderer/audio_renderer.h"
#include "audio_core/renderer/system_manager.h"
#include "core/core.h"
#include "core/hle/kernel/k_process.h"
#include "core/hle/kernel/k_transfer_memory.h"
#include "core/hle/service/audio/errors.h"
namespace AudioCore::Renderer {
Renderer::Renderer(Core::System& system_, Manager& manager_, Kernel::KEvent* rendered_event)
: core{system_}, manager{manager_}, system{system_, rendered_event} {}
Result Renderer::Initialize(const AudioRendererParameterInternal& params,
Kernel::KTransferMemory* transfer_memory,
const u64 transfer_memory_size, Kernel::KProcess* process_handle,
const u64 applet_resource_user_id, const s32 session_id) {
if (params.execution_mode == ExecutionMode::Auto) {
if (!manager.AddSystem(system)) {
LOG_ERROR(Service_Audio,
"Both Audio Render sessions are in use, cannot create any more");
return Service::Audio::ResultOutOfSessions;
}
system_registered = true;
}
initialized = true;
system.Initialize(params, transfer_memory, transfer_memory_size, process_handle,
applet_resource_user_id, session_id);
return ResultSuccess;
}
void Renderer::Finalize() {
auto session_id{system.GetSessionId()};
system.Finalize();
if (system_registered) {
manager.RemoveSystem(system);
system_registered = false;
}
manager.ReleaseSessionId(session_id);
}
System& Renderer::GetSystem() {
return system;
}
void Renderer::Start() {
system.Start();
}
void Renderer::Stop() {
system.Stop();
}
Result Renderer::RequestUpdate(std::span<const u8> input, std::span<u8> performance,
std::span<u8> output) {
return system.Update(input, performance, output);
}
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,99 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <span>
#include "audio_core/renderer/system.h"
#include "core/hle/service/audio/errors.h"
namespace Core {
class System;
}
namespace Kernel {
class KTransferMemory;
class KProcess;
} // namespace Kernel
namespace AudioCore {
struct AudioRendererParameterInternal;
namespace Renderer {
class Manager;
/**
* Audio Renderer, wraps the main audio system and is mainly responsible for handling service calls.
*/
class Renderer {
public:
explicit Renderer(Core::System& system, Manager& manager, Kernel::KEvent* rendered_event);
/**
* Initialize the renderer.
* Registers the system with the Renderer::Manager, allocates workbuffers and initializes
* everything to a default state.
*
* @param params - Input parameters to initialize the system with.
* @param transfer_memory - Game-supplied memory for all workbuffers. Unused.
* @param transfer_memory_size - Size of the transfer memory. Unused.
* @param process_handle - Process handle, also used for memory.
* @param applet_resource_user_id - Applet id for this renderer. Unused.
* @param session_id - Session id of this renderer.
* @return Result code.
*/
Result Initialize(const AudioRendererParameterInternal& params,
Kernel::KTransferMemory* transfer_memory, u64 transfer_memory_size,
Kernel::KProcess* process_handle, u64 applet_resource_user_id,
s32 session_id);
/**
* Finalize the renderer for shutdown.
*/
void Finalize();
/**
* Get the renderer's system.
*
* @return Reference to the system.
*/
System& GetSystem();
/**
* Start the renderer.
*/
void Start();
/**
* Stop the renderer.
*/
void Stop();
/**
* Update the audio renderer with new information.
* Called via RequestUpdate from the AudRen:U service.
*
* @param input - Input buffer containing the new data.
* @param performance - Optional performance buffer for outputting performance metrics.
* @param output - Output data from the renderer.
* @return Result code.
*/
Result RequestUpdate(std::span<const u8> input, std::span<u8> performance,
std::span<u8> output);
private:
/// System core
Core::System& core;
/// Manager this renderer is registered with
Manager& manager;
/// Is the audio renderer initialized?
bool initialized{};
/// Is the system registered with the manager?
bool system_registered{};
/// Audio render system, main driver of audio rendering
System system;
};
} // namespace Renderer
} // namespace AudioCore

View File

@@ -0,0 +1,193 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/common/feature_support.h"
#include "audio_core/renderer/behavior/behavior_info.h"
namespace AudioCore::Renderer {
BehaviorInfo::BehaviorInfo() : process_revision{CurrentRevision} {}
u32 BehaviorInfo::GetProcessRevisionNum() const {
return process_revision;
}
u32 BehaviorInfo::GetProcessRevision() const {
return Common::MakeMagic('R', 'E', 'V',
static_cast<char>(static_cast<u8>('0') + process_revision));
}
u32 BehaviorInfo::GetUserRevisionNum() const {
return user_revision;
}
u32 BehaviorInfo::GetUserRevision() const {
return Common::MakeMagic('R', 'E', 'V',
static_cast<char>(static_cast<u8>('0') + user_revision));
}
void BehaviorInfo::SetUserLibRevision(const u32 user_revision_) {
user_revision = GetRevisionNum(user_revision_);
}
void BehaviorInfo::ClearError() {
error_count = 0;
}
void BehaviorInfo::AppendError(const ErrorInfo& error) {
LOG_ERROR(Service_Audio, "Error during RequestUpdate, reporting code {:04X} address {:08X}",
error.error_code.raw, error.address);
if (error_count < MaxErrors) {
errors[error_count++] = error;
}
}
void BehaviorInfo::CopyErrorInfo(std::span<ErrorInfo> out_errors, u32& out_count) const {
out_count = std::min(error_count, MaxErrors);
for (size_t i = 0; i < MaxErrors; i++) {
if (i < out_count) {
out_errors[i] = errors[i];
} else {
out_errors[i] = {};
}
}
}
void BehaviorInfo::UpdateFlags(const Flags flags_) {
flags = flags_;
}
bool BehaviorInfo::IsMemoryForceMappingEnabled() const {
return flags.IsMemoryForceMappingEnabled;
}
bool BehaviorInfo::IsAdpcmLoopContextBugFixed() const {
return CheckFeatureSupported(SupportTags::AdpcmLoopContextBugFix, user_revision);
}
bool BehaviorInfo::IsSplitterSupported() const {
return CheckFeatureSupported(SupportTags::Splitter, user_revision);
}
bool BehaviorInfo::IsSplitterBugFixed() const {
return CheckFeatureSupported(SupportTags::SplitterBugFix, user_revision);
}
bool BehaviorInfo::IsEffectInfoVersion2Supported() const {
return CheckFeatureSupported(SupportTags::EffectInfoVer2, user_revision);
}
bool BehaviorInfo::IsVariadicCommandBufferSizeSupported() const {
return CheckFeatureSupported(SupportTags::AudioRendererVariadicCommandBufferSize,
user_revision);
}
bool BehaviorInfo::IsWaveBufferVer2Supported() const {
return CheckFeatureSupported(SupportTags::WaveBufferVer2, user_revision);
}
bool BehaviorInfo::IsLongSizePreDelaySupported() const {
return CheckFeatureSupported(SupportTags::LongSizePreDelay, user_revision);
}
bool BehaviorInfo::IsCommandProcessingTimeEstimatorVersion2Supported() const {
return CheckFeatureSupported(SupportTags::CommandProcessingTimeEstimatorVersion2,
user_revision);
}
bool BehaviorInfo::IsCommandProcessingTimeEstimatorVersion3Supported() const {
return CheckFeatureSupported(SupportTags::CommandProcessingTimeEstimatorVersion3,
user_revision);
}
bool BehaviorInfo::IsCommandProcessingTimeEstimatorVersion4Supported() const {
return CheckFeatureSupported(SupportTags::CommandProcessingTimeEstimatorVersion4,
user_revision);
}
bool BehaviorInfo::IsCommandProcessingTimeEstimatorVersion5Supported() const {
return CheckFeatureSupported(SupportTags::CommandProcessingTimeEstimatorVersion4,
user_revision);
}
bool BehaviorInfo::IsAudioRendererProcessingTimeLimit70PercentSupported() const {
return CheckFeatureSupported(SupportTags::AudioRendererProcessingTimeLimit70Percent,
user_revision);
}
bool BehaviorInfo::IsAudioRendererProcessingTimeLimit75PercentSupported() const {
return CheckFeatureSupported(SupportTags::AudioRendererProcessingTimeLimit75Percent,
user_revision);
}
bool BehaviorInfo::IsAudioRendererProcessingTimeLimit80PercentSupported() const {
return CheckFeatureSupported(SupportTags::AudioRendererProcessingTimeLimit80Percent,
user_revision);
}
bool BehaviorInfo::IsFlushVoiceWaveBuffersSupported() const {
return CheckFeatureSupported(SupportTags::FlushVoiceWaveBuffers, user_revision);
}
bool BehaviorInfo::IsElapsedFrameCountSupported() const {
return CheckFeatureSupported(SupportTags::ElapsedFrameCount, user_revision);
}
bool BehaviorInfo::IsPerformanceMetricsDataFormatVersion2Supported() const {
return CheckFeatureSupported(SupportTags::PerformanceMetricsDataFormatVersion2, user_revision);
}
size_t BehaviorInfo::GetPerformanceMetricsDataFormat() const {
if (CheckFeatureSupported(SupportTags::PerformanceMetricsDataFormatVersion2, user_revision)) {
return 2;
}
return 1;
}
bool BehaviorInfo::IsVoicePitchAndSrcSkippedSupported() const {
return CheckFeatureSupported(SupportTags::VoicePitchAndSrcSkipped, user_revision);
}
bool BehaviorInfo::IsVoicePlayedSampleCountResetAtLoopPointSupported() const {
return CheckFeatureSupported(SupportTags::VoicePlayedSampleCountResetAtLoopPoint,
user_revision);
}
bool BehaviorInfo::IsBiquadFilterEffectStateClearBugFixed() const {
return CheckFeatureSupported(SupportTags::BiquadFilterEffectStateClearBugFix, user_revision);
}
bool BehaviorInfo::IsVolumeMixParameterPrecisionQ23Supported() const {
return CheckFeatureSupported(SupportTags::VolumeMixParameterPrecisionQ23, user_revision);
}
bool BehaviorInfo::UseBiquadFilterFloatProcessing() const {
return CheckFeatureSupported(SupportTags::BiquadFilterFloatProcessing, user_revision);
}
bool BehaviorInfo::IsMixInParameterDirtyOnlyUpdateSupported() const {
return CheckFeatureSupported(SupportTags::MixInParameterDirtyOnlyUpdate, user_revision);
}
bool BehaviorInfo::UseMultiTapBiquadFilterProcessing() const {
return CheckFeatureSupported(SupportTags::MultiTapBiquadFilterProcessing, user_revision);
}
bool BehaviorInfo::IsDeviceApiVersion2Supported() const {
return CheckFeatureSupported(SupportTags::DeviceApiVersion2, user_revision);
}
bool BehaviorInfo::IsDelayChannelMappingChanged() const {
return CheckFeatureSupported(SupportTags::DelayChannelMappingChange, user_revision);
}
bool BehaviorInfo::IsReverbChannelMappingChanged() const {
return CheckFeatureSupported(SupportTags::ReverbChannelMappingChange, user_revision);
}
bool BehaviorInfo::IsI3dl2ReverbChannelMappingChanged() const {
return CheckFeatureSupported(SupportTags::I3dl2ReverbChannelMappingChange, user_revision);
}
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,376 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu 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"
#include "core/hle/service/audio/errors.h"
namespace AudioCore::Renderer {
/**
* Holds host and user revisions, checks whether render features can be enabled, and reports errors.
*/
class BehaviorInfo {
static constexpr u32 MaxErrors = 10;
public:
struct ErrorInfo {
/* 0x00 */ Result error_code{0};
/* 0x04 */ u32 unk_04;
/* 0x08 */ CpuAddr address;
};
static_assert(sizeof(ErrorInfo) == 0x10, "BehaviorInfo::ErrorInfo has the wrong size!");
struct Flags {
u64 IsMemoryForceMappingEnabled : 1;
};
struct InParameter {
/* 0x00 */ u32 revision;
/* 0x08 */ Flags flags;
};
static_assert(sizeof(InParameter) == 0x10, "BehaviorInfo::InParameter has the wrong size!");
struct OutStatus {
/* 0x00 */ std::array<ErrorInfo, MaxErrors> errors;
/* 0xA0 */ u32 error_count;
/* 0xA4 */ char unkA4[0xC];
};
static_assert(sizeof(OutStatus) == 0xB0, "BehaviorInfo::OutStatus has the wrong size!");
BehaviorInfo();
/**
* Get the host revision as a number.
*
* @return The host revision.
*/
u32 GetProcessRevisionNum() const;
/**
* Get the host revision in chars, e.g REV8.
* Rev 10 and higher use the ascii characters above 9.
* E.g:
* Rev 10 = REV:
* Rev 11 = REV;
*
* @return The host revision.
*/
u32 GetProcessRevision() const;
/**
* Get the user revision as a number.
*
* @return The user revision.
*/
u32 GetUserRevisionNum() const;
/**
* Get the user revision in chars, e.g REV8.
* Rev 10 and higher use the ascii characters above 9. REV: REV; etc.
*
* @return The user revision.
*/
u32 GetUserRevision() const;
/**
* Set the user revision.
*
* @param user_revision - The user's revision.
*/
void SetUserLibRevision(u32 user_revision);
/**
* Clear the current error count.
*/
void ClearError();
/**
* Append an error to the error list.
*
* @param error - The new error.
*/
void AppendError(const ErrorInfo& error);
/**
* Copy errors to the given output container.
*
* @param out_errors - Output container to receive the errors.
* @param out_count - The number of errors written.
*/
void CopyErrorInfo(std::span<ErrorInfo> out_errors, u32& out_count) const;
/**
* Update the behaviour flags.
*
* @param flags - New flags to use.
*/
void UpdateFlags(Flags flags);
/**
* Check if memory pools can be forcibly mapped.
*
* @return True if enabled, otherwise false.
*/
bool IsMemoryForceMappingEnabled() const;
/**
* Check if the ADPCM context bug is fixed.
* The ADPCM context was not being sent to the AudioRenderer, leading to incorrect scaling being
* used.
*
* @return True if fixed, otherwise false.
*/
bool IsAdpcmLoopContextBugFixed() const;
/**
* Check if the splitter is supported.
*
* @return True if supported, otherwise false.
*/
bool IsSplitterSupported() const;
/**
* Check if the splitter bug is fixed.
* Update is given the wrong number of splitter destinations, leading to invalid data
* being processed.
*
* @return True if supported, otherwise false.
*/
bool IsSplitterBugFixed() const;
/**
* Check if effects version 2 are supported.
* This gives support for returning effect states from the AudioRenderer, currently only used
* for Limiter statistics.
*
* @return True if supported, otherwise false.
*/
bool IsEffectInfoVersion2Supported() const;
/**
* Check if a variadic command buffer is supported.
* As of Rev 5 with the added optional performance metric logging, the command
* buffer can be a variable size, so take that into account for calculating its size.
*
* @return True if supported, otherwise false.
*/
bool IsVariadicCommandBufferSizeSupported() const;
/**
* Check if wave buffers version 2 are supported.
* See WaveBufferVersion1 and WaveBufferVersion2.
*
* @return True if supported, otherwise false.
*/
bool IsWaveBufferVer2Supported() const;
/**
* Check if long size pre delay is supported.
* This allows a longer initial delay time for the Reverb command.
*
* @return True if supported, otherwise false.
*/
bool IsLongSizePreDelaySupported() const;
/**
* Check if the command time estimator version 2 is supported.
*
* @return True if supported, otherwise false.
*/
bool IsCommandProcessingTimeEstimatorVersion2Supported() const;
/**
* Check if the command time estimator version 3 is supported.
*
* @return True if supported, otherwise false.
*/
bool IsCommandProcessingTimeEstimatorVersion3Supported() const;
/**
* Check if the command time estimator version 4 is supported.
*
* @return True if supported, otherwise false.
*/
bool IsCommandProcessingTimeEstimatorVersion4Supported() const;
/**
* Check if the command time estimator version 5 is supported.
*
* @return True if supported, otherwise false.
*/
bool IsCommandProcessingTimeEstimatorVersion5Supported() const;
/**
* Check if the AudioRenderer can use up to 70% of the allocated processing timeslice.
*
* @return True if supported, otherwise false.
*/
bool IsAudioRendererProcessingTimeLimit70PercentSupported() const;
/**
* Check if the AudioRenderer can use up to 75% of the allocated processing timeslice.
*
* @return True if supported, otherwise false.
*/
bool IsAudioRendererProcessingTimeLimit75PercentSupported() const;
/**
* Check if the AudioRenderer can use up to 80% of the allocated processing timeslice.
*
* @return True if supported, otherwise false.
*/
bool IsAudioRendererProcessingTimeLimit80PercentSupported() const;
/**
* Check if voice flushing is supported
* This allowws low-priority voices to be dropped if the AudioRenderer is running behind.
*
* @return True if supported, otherwise false.
*/
bool IsFlushVoiceWaveBuffersSupported() const;
/**
* Check if counting the number of elapsed frames is supported.
* This adds extra output to RequestUpdate, returning the number of times the AudioRenderer
* processed a command list.
*
* @return True if supported, otherwise false.
*/
bool IsElapsedFrameCountSupported() const;
/**
* Check if performance metrics version 2 are supported.
* This adds extra output to RequestUpdate, returning the number of times the AudioRenderer
* (Unused?).
*
* @return True if supported, otherwise false.
*/
bool IsPerformanceMetricsDataFormatVersion2Supported() const;
/**
* Get the supported performance metrics version.
* Version 2 logs some extra fields in output, such as number of voices dropped,
* processing start time, if the AudioRenderer exceeded its time, etc.
*
* @return Version supported, either 1 or 2.
*/
size_t GetPerformanceMetricsDataFormat() const;
/**
* Check if skipping voice pitch and sample rate conversion is supported.
* This speeds up the data source commands by skipping resampling if unwanted.
* See AudioCore::Renderer::DecodeFromWaveBuffers
*
* @return True if supported, otherwise false.
*/
bool IsVoicePitchAndSrcSkippedSupported() const;
/**
* Check if resetting played sample count at loop points is supported.
* This resets the number of samples played in a voice state when a loop point is reached.
* See AudioCore::Renderer::DecodeFromWaveBuffers
*
* @return True if supported, otherwise false.
*/
bool IsVoicePlayedSampleCountResetAtLoopPointSupported() const;
/**
* Check if the clear state bug for biquad filters is fixed.
* The biquad state was not marked as needing re-initialisation when the effect was updated, it
* was only initialized once with a new effect.
*
* @return True if fixed, otherwise false.
*/
bool IsBiquadFilterEffectStateClearBugFixed() const;
/**
* Check if Q23 precision is supported for fixed point.
*
* @return True if supported, otherwise false.
*/
bool IsVolumeMixParameterPrecisionQ23Supported() const;
/**
* Check if float processing for biuad filters is supported.
*
* @return True if supported, otherwise false.
*/
bool UseBiquadFilterFloatProcessing() const;
/**
* Check if dirty-only mix updates are supported.
* This saves a lot of buffer size as mixes can be large and not change much.
*
* @return True if supported, otherwise false.
*/
bool IsMixInParameterDirtyOnlyUpdateSupported() const;
/**
* Check if multi-tap biquad filters are supported.
*
* @return True if supported, otherwise false.
*/
bool UseMultiTapBiquadFilterProcessing() const;
/**
* Check if device api version 2 is supported.
* In the SDK but not in any sysmodule? Not sure, left here for completeness anyway.
*
* @return True if supported, otherwise false.
*/
bool IsDeviceApiVersion2Supported() const;
/**
* Check if new channel mappings are used for Delay commands.
* Older commands used:
* front left/front right/back left/back right/center/lfe
* Whereas everywhere else in the code uses:
* front left/front right/center/lfe/back left/back right
* This corrects that and makes everything standardised.
*
* @return True if supported, otherwise false.
*/
bool IsDelayChannelMappingChanged() const;
/**
* Check if new channel mappings are used for Reverb commands.
* Older commands used:
* front left/front right/back left/back right/center/lfe
* Whereas everywhere else in the code uses:
* front left/front right/center/lfe/back left/back right
* This corrects that and makes everything standardised.
*
* @return True if supported, otherwise false.
*/
bool IsReverbChannelMappingChanged() const;
/**
* Check if new channel mappings are used for I3dl2Reverb commands.
* Older commands used:
* front left/front right/back left/back right/center/lfe
* Whereas everywhere else in the code uses:
* front left/front right/center/lfe/back left/back right
* This corrects that and makes everything standardised.
*
* @return True if supported, otherwise false.
*/
bool IsI3dl2ReverbChannelMappingChanged() const;
/// Host version
u32 process_revision;
/// User version
u32 user_revision{};
/// Behaviour flags
Flags flags{};
/// Errors generated and reported during Update
std::array<ErrorInfo, MaxErrors> errors{};
/// Error count
u32 error_count{};
};
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,539 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/common/feature_support.h"
#include "audio_core/renderer/behavior/behavior_info.h"
#include "audio_core/renderer/behavior/info_updater.h"
#include "audio_core/renderer/effect/effect_context.h"
#include "audio_core/renderer/effect/effect_reset.h"
#include "audio_core/renderer/memory/memory_pool_info.h"
#include "audio_core/renderer/mix/mix_context.h"
#include "audio_core/renderer/performance/performance_manager.h"
#include "audio_core/renderer/sink/circular_buffer_sink_info.h"
#include "audio_core/renderer/sink/device_sink_info.h"
#include "audio_core/renderer/sink/sink_context.h"
#include "audio_core/renderer/splitter/splitter_context.h"
#include "audio_core/renderer/voice/voice_context.h"
namespace AudioCore::Renderer {
InfoUpdater::InfoUpdater(std::span<const u8> input_, std::span<u8> output_,
Kernel::KProcess* process_handle_, BehaviorInfo& behaviour_)
: input{input_.data() + sizeof(UpdateDataHeader)},
input_origin{input_}, output{output_.data() + sizeof(UpdateDataHeader)},
output_origin{output_}, in_header{reinterpret_cast<const UpdateDataHeader*>(
input_origin.data())},
out_header{reinterpret_cast<UpdateDataHeader*>(output_origin.data())},
expected_input_size{input_.size()}, expected_output_size{output_.size()},
process_handle{process_handle_}, behaviour{behaviour_} {
std::construct_at<UpdateDataHeader>(out_header, behaviour.GetProcessRevision());
}
Result InfoUpdater::UpdateVoiceChannelResources(VoiceContext& voice_context) {
const auto voice_count{voice_context.GetCount()};
std::span<const VoiceChannelResource::InParameter> in_params{
reinterpret_cast<const VoiceChannelResource::InParameter*>(input), voice_count};
for (u32 i = 0; i < voice_count; i++) {
auto& resource{voice_context.GetChannelResource(i)};
resource.in_use = in_params[i].in_use;
if (in_params[i].in_use) {
resource.mix_volumes = in_params[i].mix_volumes;
}
}
const auto consumed_input_size{voice_count *
static_cast<u32>(sizeof(VoiceChannelResource::InParameter))};
if (consumed_input_size != in_header->voice_resources_size) {
LOG_ERROR(Service_Audio,
"Consumed an incorrect voice resource size, header size={}, consumed={}",
in_header->voice_resources_size, consumed_input_size);
return Service::Audio::ResultInvalidUpdateInfo;
}
input += consumed_input_size;
return ResultSuccess;
}
Result InfoUpdater::UpdateVoices(VoiceContext& voice_context,
std::span<MemoryPoolInfo> memory_pools,
const u32 memory_pool_count) {
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};
for (u32 i = 0; i < voice_count; i++) {
auto& voice_info{voice_context.GetInfo(i)};
voice_info.in_use = false;
}
u32 new_voice_count{0};
for (u32 i = 0; i < voice_count; i++) {
const auto& in_param{in_params[i]};
std::array<VoiceState*, MaxChannels> voice_states{};
if (!in_param.in_use) {
continue;
}
auto& voice_info{voice_context.GetInfo(in_param.id)};
for (u32 channel = 0; channel < in_param.channel_count; channel++) {
voice_states[channel] = &voice_context.GetState(in_param.channel_resource_ids[channel]);
}
if (in_param.is_new) {
voice_info.Initialize();
for (u32 channel = 0; channel < in_param.channel_count; channel++) {
*voice_states[channel] = {};
}
}
BehaviorInfo::ErrorInfo update_error{};
voice_info.UpdateParameters(update_error, in_param, pool_mapper, behaviour);
if (!update_error.error_code.IsSuccess()) {
behaviour.AppendError(update_error);
}
std::array<std::array<BehaviorInfo::ErrorInfo, 2>, MaxWaveBuffers> wavebuffer_errors{};
voice_info.UpdateWaveBuffers(wavebuffer_errors, MaxWaveBuffers * 2, in_param, voice_states,
pool_mapper, behaviour);
for (auto& wavebuffer_error : wavebuffer_errors) {
for (auto& error : wavebuffer_error) {
if (error.error_code.IsError()) {
behaviour.AppendError(error);
}
}
}
voice_info.WriteOutStatus(out_params[i], in_param, voice_states);
new_voice_count += in_param.channel_count;
}
auto consumed_input_size{voice_count * static_cast<u32>(sizeof(VoiceInfo::InParameter))};
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={}",
in_header->voices_size, consumed_input_size);
return Service::Audio::ResultInvalidUpdateInfo;
}
out_header->voices_size = consumed_output_size;
out_header->size += consumed_output_size;
input += consumed_input_size;
output += consumed_output_size;
voice_context.SetActiveCount(new_voice_count);
return ResultSuccess;
}
Result InfoUpdater::UpdateEffects(EffectContext& effect_context, const bool renderer_active,
std::span<MemoryPoolInfo> memory_pools,
const u32 memory_pool_count) {
if (behaviour.IsEffectInfoVersion2Supported()) {
return UpdateEffectsVersion2(effect_context, renderer_active, memory_pools,
memory_pool_count);
} else {
return UpdateEffectsVersion1(effect_context, renderer_active, memory_pools,
memory_pool_count);
}
}
Result InfoUpdater::UpdateEffectsVersion1(EffectContext& effect_context, const bool renderer_active,
std::span<MemoryPoolInfo> memory_pools,
const u32 memory_pool_count) {
PoolMapper pool_mapper(process_handle, memory_pools, memory_pool_count,
behaviour.IsMemoryForceMappingEnabled());
const auto effect_count{effect_context.GetCount()};
std::span<const EffectInfoBase::InParameterVersion1> in_params{
reinterpret_cast<const EffectInfoBase::InParameterVersion1*>(input), effect_count};
std::span<EffectInfoBase::OutStatusVersion1> out_params{
reinterpret_cast<EffectInfoBase::OutStatusVersion1*>(output), effect_count};
for (u32 i = 0; i < effect_count; i++) {
auto effect_info{&effect_context.GetInfo(i)};
if (effect_info->GetType() != in_params[i].type) {
effect_info->ForceUnmapBuffers(pool_mapper);
ResetEffect(effect_info, in_params[i].type);
}
BehaviorInfo::ErrorInfo error_info{};
effect_info->Update(error_info, in_params[i], pool_mapper);
if (error_info.error_code.IsError()) {
behaviour.AppendError(error_info);
}
effect_info->StoreStatus(out_params[i], renderer_active);
}
auto consumed_input_size{effect_count *
static_cast<u32>(sizeof(EffectInfoBase::InParameterVersion1))};
auto consumed_output_size{effect_count *
static_cast<u32>(sizeof(EffectInfoBase::OutStatusVersion1))};
if (consumed_input_size != in_header->effects_size) {
LOG_ERROR(Service_Audio, "Consumed an incorrect effects size, header size={}, consumed={}",
in_header->effects_size, consumed_input_size);
return Service::Audio::ResultInvalidUpdateInfo;
}
out_header->effects_size = consumed_output_size;
out_header->size += consumed_output_size;
input += consumed_input_size;
output += consumed_output_size;
return ResultSuccess;
}
Result InfoUpdater::UpdateEffectsVersion2(EffectContext& effect_context, const bool renderer_active,
std::span<MemoryPoolInfo> memory_pools,
const u32 memory_pool_count) {
PoolMapper pool_mapper(process_handle, memory_pools, memory_pool_count,
behaviour.IsMemoryForceMappingEnabled());
const auto effect_count{effect_context.GetCount()};
std::span<const EffectInfoBase::InParameterVersion2> in_params{
reinterpret_cast<const EffectInfoBase::InParameterVersion2*>(input), effect_count};
std::span<EffectInfoBase::OutStatusVersion2> out_params{
reinterpret_cast<EffectInfoBase::OutStatusVersion2*>(output), effect_count};
for (u32 i = 0; i < effect_count; i++) {
auto effect_info{&effect_context.GetInfo(i)};
if (effect_info->GetType() != in_params[i].type) {
effect_info->ForceUnmapBuffers(pool_mapper);
ResetEffect(effect_info, in_params[i].type);
}
BehaviorInfo::ErrorInfo error_info{};
effect_info->Update(error_info, in_params[i], pool_mapper);
if (error_info.error_code.IsError()) {
behaviour.AppendError(error_info);
}
effect_info->StoreStatus(out_params[i], renderer_active);
if (in_params[i].is_new) {
effect_info->InitializeResultState(effect_context.GetDspSharedResultState(i));
effect_info->InitializeResultState(effect_context.GetResultState(i));
}
effect_info->UpdateResultState(out_params[i].result_state,
effect_context.GetResultState(i));
}
auto consumed_input_size{effect_count *
static_cast<u32>(sizeof(EffectInfoBase::InParameterVersion2))};
auto consumed_output_size{effect_count *
static_cast<u32>(sizeof(EffectInfoBase::OutStatusVersion2))};
if (consumed_input_size != in_header->effects_size) {
LOG_ERROR(Service_Audio, "Consumed an incorrect effects size, header size={}, consumed={}",
in_header->effects_size, consumed_input_size);
return Service::Audio::ResultInvalidUpdateInfo;
}
out_header->effects_size = consumed_output_size;
out_header->size += consumed_output_size;
input += consumed_input_size;
output += consumed_output_size;
return ResultSuccess;
}
Result InfoUpdater::UpdateMixes(MixContext& mix_context, const u32 mix_buffer_count,
EffectContext& effect_context, SplitterContext& splitter_context) {
s32 mix_count{0};
u32 consumed_input_size{0};
if (behaviour.IsMixInParameterDirtyOnlyUpdateSupported()) {
auto in_dirty_params{reinterpret_cast<const MixInfo::InDirtyParameter*>(input)};
mix_count = in_dirty_params->count;
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));
}
if (mix_buffer_count == 0) {
return Service::Audio::ResultInvalidUpdateInfo;
}
std::span<const MixInfo::InParameter> in_params{
reinterpret_cast<const MixInfo::InParameter*>(input), static_cast<size_t>(mix_count)};
u32 total_buffer_count{0};
for (s32 i = 0; i < mix_count; i++) {
const auto& params{in_params[i]};
if (params.in_use) {
total_buffer_count += params.buffer_count;
if (params.dest_mix_id > static_cast<s32>(mix_context.GetCount()) &&
params.dest_mix_id != UnusedMixId && params.mix_id != FinalMixId) {
return Service::Audio::ResultInvalidUpdateInfo;
}
}
}
if (total_buffer_count > mix_buffer_count) {
return Service::Audio::ResultInvalidUpdateInfo;
}
bool mix_dirty{false};
for (s32 i = 0; i < mix_count; i++) {
const auto& params{in_params[i]};
s32 mix_id{i};
if (behaviour.IsMixInParameterDirtyOnlyUpdateSupported()) {
mix_id = params.mix_id;
}
auto mix_info{mix_context.GetInfo(mix_id)};
if (mix_info->in_use != params.in_use) {
mix_info->in_use = params.in_use;
if (!params.in_use) {
mix_info->ClearEffectProcessingOrder();
}
mix_dirty = true;
}
if (params.in_use) {
mix_dirty |= mix_info->Update(mix_context.GetEdgeMatrix(), params, effect_context,
splitter_context, behaviour);
}
}
if (mix_dirty) {
if (behaviour.IsSplitterSupported() && splitter_context.UsingSplitter()) {
if (!mix_context.TSortInfo(splitter_context)) {
return Service::Audio::ResultInvalidUpdateInfo;
}
} else {
mix_context.SortInfo();
}
}
if (consumed_input_size != in_header->mix_size) {
LOG_ERROR(Service_Audio, "Consumed an incorrect mixes size, header size={}, consumed={}",
in_header->mix_size, consumed_input_size);
return Service::Audio::ResultInvalidUpdateInfo;
}
input += mix_count * sizeof(MixInfo::InParameter);
return ResultSuccess;
}
Result InfoUpdater::UpdateSinks(SinkContext& sink_context, std::span<MemoryPoolInfo> memory_pools,
const u32 memory_pool_count) {
PoolMapper pool_mapper(process_handle, memory_pools, memory_pool_count,
behaviour.IsMemoryForceMappingEnabled());
std::span<const SinkInfoBase::InParameter> in_params{
reinterpret_cast<const SinkInfoBase::InParameter*>(input), memory_pool_count};
std::span<SinkInfoBase::OutStatus> out_params{
reinterpret_cast<SinkInfoBase::OutStatus*>(output), memory_pool_count};
const auto sink_count{sink_context.GetCount()};
for (u32 i = 0; i < sink_count; i++) {
const auto& params{in_params[i]};
auto sink_info{sink_context.GetInfo(i)};
if (sink_info->GetType() != params.type) {
sink_info->CleanUp();
switch (params.type) {
case SinkInfoBase::Type::Invalid:
std::construct_at<SinkInfoBase>(reinterpret_cast<SinkInfoBase*>(sink_info));
break;
case SinkInfoBase::Type::DeviceSink:
std::construct_at<DeviceSinkInfo>(reinterpret_cast<DeviceSinkInfo*>(sink_info));
break;
case SinkInfoBase::Type::CircularBufferSink:
std::construct_at<CircularBufferSinkInfo>(
reinterpret_cast<CircularBufferSinkInfo*>(sink_info));
break;
default:
LOG_ERROR(Service_Audio, "Invalid sink type {}", static_cast<u32>(params.type));
break;
}
}
BehaviorInfo::ErrorInfo error_info{};
sink_info->Update(error_info, out_params[i], params, pool_mapper);
if (error_info.error_code.IsError()) {
behaviour.AppendError(error_info);
}
}
const auto consumed_input_size{sink_count *
static_cast<u32>(sizeof(SinkInfoBase::InParameter))};
const auto consumed_output_size{sink_count * static_cast<u32>(sizeof(SinkInfoBase::OutStatus))};
if (consumed_input_size != in_header->sinks_size) {
LOG_ERROR(Service_Audio, "Consumed an incorrect sinks size, header size={}, consumed={}",
in_header->sinks_size, consumed_input_size);
return Service::Audio::ResultInvalidUpdateInfo;
}
input += consumed_input_size;
output += consumed_output_size;
out_header->sinks_size = consumed_output_size;
out_header->size += consumed_output_size;
return ResultSuccess;
}
Result InfoUpdater::UpdateMemoryPools(std::span<MemoryPoolInfo> memory_pools,
const u32 memory_pool_count) {
PoolMapper pool_mapper(process_handle, memory_pools, memory_pool_count,
behaviour.IsMemoryForceMappingEnabled());
std::span<const MemoryPoolInfo::InParameter> in_params{
reinterpret_cast<const MemoryPoolInfo::InParameter*>(input), memory_pool_count};
std::span<MemoryPoolInfo::OutStatus> out_params{
reinterpret_cast<MemoryPoolInfo::OutStatus*>(output), memory_pool_count};
for (size_t i = 0; i < memory_pool_count; i++) {
auto state{pool_mapper.Update(memory_pools[i], in_params[i], out_params[i])};
if (state != MemoryPoolInfo::ResultState::Success &&
state != MemoryPoolInfo::ResultState::BadParam &&
state != MemoryPoolInfo::ResultState::MapFailed &&
state != MemoryPoolInfo::ResultState::InUse) {
LOG_WARNING(Service_Audio, "Invalid ResultState from updating memory pools");
return Service::Audio::ResultInvalidUpdateInfo;
}
}
const auto consumed_input_size{memory_pool_count *
static_cast<u32>(sizeof(MemoryPoolInfo::InParameter))};
const auto consumed_output_size{memory_pool_count *
static_cast<u32>(sizeof(MemoryPoolInfo::OutStatus))};
if (consumed_input_size != in_header->memory_pool_size) {
LOG_ERROR(Service_Audio,
"Consumed an incorrect memory pool size, header size={}, consumed={}",
in_header->memory_pool_size, consumed_input_size);
return Service::Audio::ResultInvalidUpdateInfo;
}
input += consumed_input_size;
output += consumed_output_size;
out_header->memory_pool_size = consumed_output_size;
out_header->size += consumed_output_size;
return ResultSuccess;
}
Result InfoUpdater::UpdatePerformanceBuffer(std::span<u8> performance_output,
const u64 performance_output_size,
PerformanceManager* performance_manager) {
auto in_params{reinterpret_cast<const PerformanceManager::InParameter*>(input)};
auto out_params{reinterpret_cast<PerformanceManager::OutStatus*>(output)};
if (performance_manager != nullptr) {
out_params->history_size =
performance_manager->CopyHistories(performance_output.data(), performance_output_size);
performance_manager->SetDetailTarget(in_params->target_node_id);
} else {
out_params->history_size = 0;
}
const auto consumed_input_size{static_cast<u32>(sizeof(PerformanceManager::InParameter))};
const auto consumed_output_size{static_cast<u32>(sizeof(PerformanceManager::OutStatus))};
if (consumed_input_size != in_header->performance_buffer_size) {
LOG_ERROR(Service_Audio,
"Consumed an incorrect performance size, header size={}, consumed={}",
in_header->performance_buffer_size, consumed_input_size);
return Service::Audio::ResultInvalidUpdateInfo;
}
input += consumed_input_size;
output += consumed_output_size;
out_header->performance_buffer_size = consumed_output_size;
out_header->size += consumed_output_size;
return ResultSuccess;
}
Result InfoUpdater::UpdateBehaviorInfo(BehaviorInfo& behaviour_) {
const auto in_params{reinterpret_cast<const BehaviorInfo::InParameter*>(input)};
if (!CheckValidRevision(in_params->revision)) {
return Service::Audio::ResultInvalidUpdateInfo;
}
if (in_params->revision != behaviour_.GetUserRevision()) {
return Service::Audio::ResultInvalidUpdateInfo;
}
behaviour_.ClearError();
behaviour_.UpdateFlags(in_params->flags);
if (in_header->behaviour_size != sizeof(BehaviorInfo::InParameter)) {
return Service::Audio::ResultInvalidUpdateInfo;
}
input += sizeof(BehaviorInfo::InParameter);
return ResultSuccess;
}
Result InfoUpdater::UpdateErrorInfo(const BehaviorInfo& behaviour_) {
auto out_params{reinterpret_cast<BehaviorInfo::OutStatus*>(output)};
behaviour_.CopyErrorInfo(out_params->errors, out_params->error_count);
const auto consumed_output_size{static_cast<u32>(sizeof(BehaviorInfo::OutStatus))};
output += consumed_output_size;
out_header->behaviour_size = consumed_output_size;
out_header->size += consumed_output_size;
return ResultSuccess;
}
Result InfoUpdater::UpdateSplitterInfo(SplitterContext& splitter_context) {
u32 consumed_size{0};
if (!splitter_context.Update(input, consumed_size)) {
return Service::Audio::ResultInvalidUpdateInfo;
}
input += consumed_size;
return ResultSuccess;
}
Result InfoUpdater::UpdateRendererInfo(const u64 elapsed_frames) {
struct RenderInfo {
/* 0x00 */ u64 frames_elapsed;
/* 0x08 */ char unk08[0x8];
};
static_assert(sizeof(RenderInfo) == 0x10, "RenderInfo has the wrong size!");
auto out_params{reinterpret_cast<RenderInfo*>(output)};
out_params->frames_elapsed = elapsed_frames;
const auto consumed_output_size{static_cast<u32>(sizeof(RenderInfo))};
output += consumed_output_size;
out_header->render_info_size = consumed_output_size;
out_header->size += consumed_output_size;
return ResultSuccess;
}
Result InfoUpdater::CheckConsumedSize() {
if (CpuAddr(input) - CpuAddr(input_origin.data()) != expected_input_size) {
return Service::Audio::ResultInvalidUpdateInfo;
} else if (CpuAddr(output) - CpuAddr(output_origin.data()) != expected_output_size) {
return Service::Audio::ResultInvalidUpdateInfo;
}
return ResultSuccess;
}
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,209 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <span>
#include "common/common_types.h"
#include "core/hle/service/audio/errors.h"
namespace Kernel {
class KProcess;
}
namespace AudioCore::Renderer {
class BehaviorInfo;
class VoiceContext;
class MixContext;
class SinkContext;
class SplitterContext;
class EffectContext;
class MemoryPoolInfo;
class PerformanceManager;
class InfoUpdater {
struct UpdateDataHeader {
explicit UpdateDataHeader(u32 revision_) : revision{revision_} {}
/* 0x00 */ u32 revision;
/* 0x04 */ u32 behaviour_size{};
/* 0x08 */ u32 memory_pool_size{};
/* 0x0C */ u32 voices_size{};
/* 0x10 */ u32 voice_resources_size{};
/* 0x14 */ u32 effects_size{};
/* 0x18 */ u32 mix_size{};
/* 0x1C */ u32 sinks_size{};
/* 0x20 */ u32 performance_buffer_size{};
/* 0x24 */ char unk24[4];
/* 0x28 */ u32 render_info_size{};
/* 0x2C */ char unk2C[0x10];
/* 0x3C */ u32 size{sizeof(UpdateDataHeader)};
};
static_assert(sizeof(UpdateDataHeader) == 0x40, "UpdateDataHeader has the wrong size!");
public:
explicit InfoUpdater(std::span<const u8> input, std::span<u8> output,
Kernel::KProcess* process_handle, BehaviorInfo& behaviour);
/**
* Update the voice channel resources.
*
* @param voice_context - Voice context to update.
* @return Result code.
*/
Result UpdateVoiceChannelResources(VoiceContext& voice_context);
/**
* Update voices.
*
* @param voice_context - Voice context to update.
* @param memory_pools - Memory pools to use for these voices.
* @param memory_pool_count - Number of memory pools.
* @return Result code.
*/
Result UpdateVoices(VoiceContext& voice_context, std::span<MemoryPoolInfo> memory_pools,
u32 memory_pool_count);
/**
* Update effects.
*
* @param effect_context - Effect context to update.
* @param renderer_active - Whether the AudioRenderer is active.
* @param memory_pools - Memory pools to use for these voices.
* @param memory_pool_count - Number of memory pools.
* @return Result code.
*/
Result UpdateEffects(EffectContext& effect_context, bool renderer_active,
std::span<MemoryPoolInfo> memory_pools, u32 memory_pool_count);
/**
* Update mixes.
*
* @param mix_context - Mix context to update.
* @param mix_buffer_count - Number of mix buffers.
* @param effect_context - Effect context to update effort order.
* @param splitter_context - Splitter context for the mixes.
* @return Result code.
*/
Result UpdateMixes(MixContext& mix_context, u32 mix_buffer_count, EffectContext& effect_context,
SplitterContext& splitter_context);
/**
* Update sinks.
*
* @param sink_context - Sink context to update.
* @param memory_pools - Memory pools to use for these voices.
* @param memory_pool_count - Number of memory pools.
* @return Result code.
*/
Result UpdateSinks(SinkContext& sink_context, std::span<MemoryPoolInfo> memory_pools,
u32 memory_pool_count);
/**
* Update memory pools.
*
* @param memory_pools - Memory pools to use for these voices.
* @param memory_pool_count - Number of memory pools.
* @return Result code.
*/
Result UpdateMemoryPools(std::span<MemoryPoolInfo> memory_pools, u32 memory_pool_count);
/**
* Update the performance buffer.
*
* @param output - Output buffer for performance metrics.
* @param output_size - Output buffer size.
* @param performance_manager - Performance manager..
* @return Result code.
*/
Result UpdatePerformanceBuffer(std::span<u8> output, u64 output_size,
PerformanceManager* performance_manager);
/**
* Update behaviour.
*
* @param behaviour - Behaviour to update.
* @return Result code.
*/
Result UpdateBehaviorInfo(BehaviorInfo& behaviour);
/**
* Update errors.
*
* @param behaviour - Behaviour to update.
* @return Result code.
*/
Result UpdateErrorInfo(const BehaviorInfo& behaviour);
/**
* Update splitter.
*
* @param splitter_context - Splitter context to update.
* @return Result code.
*/
Result UpdateSplitterInfo(SplitterContext& splitter_context);
/**
* Update renderer info.
*
* @param elapsed_frames - Number of elapsed frames.
* @return Result code.
*/
Result UpdateRendererInfo(u64 elapsed_frames);
/**
* Check that the input.output sizes match their expected values.
*
* @return Result code.
*/
Result CheckConsumedSize();
private:
/**
* Update effects version 1.
*
* @param effect_context - Effect context to update.
* @param renderer_active - Is the AudioRenderer active?
* @param memory_pools - Memory pools to use for these voices.
* @param memory_pool_count - Number of memory pools.
* @return Result code.
*/
Result UpdateEffectsVersion1(EffectContext& effect_context, bool renderer_active,
std::span<MemoryPoolInfo> memory_pools, u32 memory_pool_count);
/**
* Update effects version 2.
*
* @param effect_context - Effect context to update.
* @param renderer_active - Is the AudioRenderer active?
* @param memory_pools - Memory pools to use for these voices.
* @param memory_pool_count - Number of memory pools.
* @return Result code.
*/
Result UpdateEffectsVersion2(EffectContext& effect_context, bool renderer_active,
std::span<MemoryPoolInfo> memory_pools, u32 memory_pool_count);
/// Input buffer
u8 const* input;
/// Input buffer start
std::span<const u8> input_origin;
/// Output buffer start
u8* output;
/// Output buffer start
std::span<u8> output_origin;
/// Input header
const UpdateDataHeader* in_header;
/// Output header
UpdateDataHeader* out_header;
/// Expected input size, see CheckConsumedSize
u64 expected_input_size;
/// Expected output size, see CheckConsumedSize
u64 expected_output_size;
/// Unused
Kernel::KProcess* process_handle;
/// Behaviour
BehaviorInfo& behaviour;
};
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,716 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/renderer/behavior/behavior_info.h"
#include "audio_core/renderer/command/command_buffer.h"
#include "audio_core/renderer/command/command_list_header.h"
#include "audio_core/renderer/command/command_processing_time_estimator.h"
#include "audio_core/renderer/effect/biquad_filter.h"
#include "audio_core/renderer/effect/delay.h"
#include "audio_core/renderer/effect/reverb.h"
#include "audio_core/renderer/memory/memory_pool_info.h"
#include "audio_core/renderer/mix/mix_info.h"
#include "audio_core/renderer/sink/circular_buffer_sink_info.h"
#include "audio_core/renderer/sink/device_sink_info.h"
#include "audio_core/renderer/sink/sink_info_base.h"
#include "audio_core/renderer/voice/voice_info.h"
#include "audio_core/renderer/voice/voice_state.h"
namespace AudioCore::Renderer {
template <typename T, CommandId Id>
T& CommandBuffer::GenerateStart(const s32 node_id) {
if (size + sizeof(T) >= command_list.size_bytes()) {
LOG_ERROR(
Service_Audio,
"Attempting to write commands beyond the end of allocated command buffer memory!");
UNREACHABLE();
}
auto& cmd{*std::construct_at<T>(reinterpret_cast<T*>(&command_list[size]))};
cmd.magic = CommandMagic;
cmd.enabled = true;
cmd.type = Id;
cmd.size = sizeof(T);
cmd.node_id = node_id;
return cmd;
}
template <typename T>
void CommandBuffer::GenerateEnd(T& cmd) {
cmd.estimated_process_time = time_estimator->Estimate(cmd);
estimated_process_time += cmd.estimated_process_time;
size += sizeof(T);
count++;
}
void CommandBuffer::GeneratePcmInt16Version1Command(const s32 node_id,
const MemoryPoolInfo& memory_pool_,
VoiceInfo& voice_info,
const VoiceState& voice_state,
const s16 buffer_count, const s8 channel) {
auto& cmd{
GenerateStart<PcmInt16DataSourceVersion1Command, CommandId::DataSourcePcmInt16Version1>(
node_id)};
cmd.src_quality = voice_info.src_quality;
cmd.output_index = buffer_count + channel;
cmd.flags = voice_info.flags & 3;
cmd.sample_rate = voice_info.sample_rate;
cmd.pitch = voice_info.pitch;
cmd.channel_index = channel;
cmd.channel_count = voice_info.channel_count;
for (u32 i = 0; i < MaxWaveBuffers; i++) {
voice_info.wavebuffers[i].Copy(cmd.wave_buffers[i]);
}
cmd.voice_state = memory_pool_.Translate(CpuAddr(&voice_state), sizeof(VoiceState));
GenerateEnd<PcmInt16DataSourceVersion1Command>(cmd);
}
void CommandBuffer::GeneratePcmInt16Version2Command(const s32 node_id, VoiceInfo& voice_info,
const VoiceState& voice_state,
const s16 buffer_count, const s8 channel) {
auto& cmd{
GenerateStart<PcmInt16DataSourceVersion2Command, CommandId::DataSourcePcmInt16Version2>(
node_id)};
cmd.src_quality = voice_info.src_quality;
cmd.output_index = buffer_count + channel;
cmd.flags = voice_info.flags & 3;
cmd.sample_rate = voice_info.sample_rate;
cmd.pitch = voice_info.pitch;
cmd.channel_index = channel;
cmd.channel_count = voice_info.channel_count;
for (u32 i = 0; i < MaxWaveBuffers; i++) {
voice_info.wavebuffers[i].Copy(cmd.wave_buffers[i]);
}
cmd.voice_state = memory_pool->Translate(CpuAddr(&voice_state), sizeof(VoiceState));
GenerateEnd<PcmInt16DataSourceVersion2Command>(cmd);
}
void CommandBuffer::GeneratePcmFloatVersion1Command(const s32 node_id,
const MemoryPoolInfo& memory_pool_,
VoiceInfo& voice_info,
const VoiceState& voice_state,
const s16 buffer_count, const s8 channel) {
auto& cmd{
GenerateStart<PcmFloatDataSourceVersion1Command, CommandId::DataSourcePcmFloatVersion1>(
node_id)};
cmd.src_quality = voice_info.src_quality;
cmd.output_index = buffer_count + channel;
cmd.flags = voice_info.flags & 3;
cmd.sample_rate = voice_info.sample_rate;
cmd.pitch = voice_info.pitch;
cmd.channel_index = channel;
cmd.channel_count = voice_info.channel_count;
for (u32 i = 0; i < MaxWaveBuffers; i++) {
voice_info.wavebuffers[i].Copy(cmd.wave_buffers[i]);
}
cmd.voice_state = memory_pool_.Translate(CpuAddr(&voice_state), sizeof(VoiceState));
GenerateEnd<PcmFloatDataSourceVersion1Command>(cmd);
}
void CommandBuffer::GeneratePcmFloatVersion2Command(const s32 node_id, VoiceInfo& voice_info,
const VoiceState& voice_state,
const s16 buffer_count, const s8 channel) {
auto& cmd{
GenerateStart<PcmFloatDataSourceVersion2Command, CommandId::DataSourcePcmFloatVersion2>(
node_id)};
cmd.src_quality = voice_info.src_quality;
cmd.output_index = buffer_count + channel;
cmd.flags = voice_info.flags & 3;
cmd.sample_rate = voice_info.sample_rate;
cmd.pitch = voice_info.pitch;
cmd.channel_index = channel;
cmd.channel_count = voice_info.channel_count;
for (u32 i = 0; i < MaxWaveBuffers; i++) {
voice_info.wavebuffers[i].Copy(cmd.wave_buffers[i]);
}
cmd.voice_state = memory_pool->Translate(CpuAddr(&voice_state), sizeof(VoiceState));
GenerateEnd<PcmFloatDataSourceVersion2Command>(cmd);
}
void CommandBuffer::GenerateAdpcmVersion1Command(const s32 node_id,
const MemoryPoolInfo& memory_pool_,
VoiceInfo& voice_info,
const VoiceState& voice_state,
const s16 buffer_count, const s8 channel) {
auto& cmd{
GenerateStart<AdpcmDataSourceVersion1Command, CommandId::DataSourceAdpcmVersion1>(node_id)};
cmd.src_quality = voice_info.src_quality;
cmd.output_index = buffer_count + channel;
cmd.flags = voice_info.flags & 3;
cmd.sample_rate = voice_info.sample_rate;
cmd.pitch = voice_info.pitch;
for (u32 i = 0; i < MaxWaveBuffers; i++) {
voice_info.wavebuffers[i].Copy(cmd.wave_buffers[i]);
}
cmd.voice_state = memory_pool_.Translate(CpuAddr(&voice_state), sizeof(VoiceState));
cmd.data_address = voice_info.data_address.GetReference(true);
cmd.data_size = voice_info.data_address.GetSize();
GenerateEnd<AdpcmDataSourceVersion1Command>(cmd);
}
void CommandBuffer::GenerateAdpcmVersion2Command(const s32 node_id, VoiceInfo& voice_info,
const VoiceState& voice_state,
const s16 buffer_count, const s8 channel) {
auto& cmd{
GenerateStart<AdpcmDataSourceVersion2Command, CommandId::DataSourceAdpcmVersion2>(node_id)};
cmd.src_quality = voice_info.src_quality;
cmd.output_index = buffer_count + channel;
cmd.flags = voice_info.flags & 3;
cmd.sample_rate = voice_info.sample_rate;
cmd.pitch = voice_info.pitch;
cmd.channel_index = channel;
cmd.channel_count = voice_info.channel_count;
for (u32 i = 0; i < MaxWaveBuffers; i++) {
voice_info.wavebuffers[i].Copy(cmd.wave_buffers[i]);
}
cmd.voice_state = memory_pool->Translate(CpuAddr(&voice_state), sizeof(VoiceState));
cmd.data_address = voice_info.data_address.GetReference(true);
cmd.data_size = voice_info.data_address.GetSize();
GenerateEnd<AdpcmDataSourceVersion2Command>(cmd);
}
void CommandBuffer::GenerateVolumeCommand(const s32 node_id, const s16 buffer_offset,
const s16 input_index, const f32 volume,
const u8 precision) {
auto& cmd{GenerateStart<VolumeCommand, CommandId::Volume>(node_id)};
cmd.precision = precision;
cmd.input_index = buffer_offset + input_index;
cmd.output_index = buffer_offset + input_index;
cmd.volume = volume;
GenerateEnd<VolumeCommand>(cmd);
}
void CommandBuffer::GenerateVolumeRampCommand(const s32 node_id, VoiceInfo& voice_info,
const s16 buffer_count, const u8 precision) {
auto& cmd{GenerateStart<VolumeRampCommand, CommandId::VolumeRamp>(node_id)};
cmd.input_index = buffer_count;
cmd.output_index = buffer_count;
cmd.prev_volume = voice_info.prev_volume;
cmd.volume = voice_info.volume;
cmd.precision = precision;
GenerateEnd<VolumeRampCommand>(cmd);
}
void CommandBuffer::GenerateBiquadFilterCommand(const s32 node_id, VoiceInfo& voice_info,
const VoiceState& voice_state,
const s16 buffer_count, const s8 channel,
const u32 biquad_index,
const bool use_float_processing) {
auto& cmd{GenerateStart<BiquadFilterCommand, CommandId::BiquadFilter>(node_id)};
cmd.input = buffer_count + channel;
cmd.output = buffer_count + channel;
cmd.biquad = voice_info.biquads[biquad_index];
cmd.state = memory_pool->Translate(CpuAddr(voice_state.biquad_states[biquad_index].data()),
MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState));
cmd.needs_init = !voice_info.biquad_initialized[biquad_index];
cmd.use_float_processing = use_float_processing;
GenerateEnd<BiquadFilterCommand>(cmd);
}
void CommandBuffer::GenerateBiquadFilterCommand(const s32 node_id, EffectInfoBase& effect_info,
const s16 buffer_offset, const s8 channel,
const bool needs_init,
const bool use_float_processing) {
auto& cmd{GenerateStart<BiquadFilterCommand, CommandId::BiquadFilter>(node_id)};
const auto& parameter{
*reinterpret_cast<BiquadFilterInfo::ParameterVersion1*>(effect_info.GetParameter())};
const auto state{reinterpret_cast<VoiceState::BiquadFilterState*>(
effect_info.GetStateBuffer() + channel * sizeof(VoiceState::BiquadFilterState))};
cmd.input = buffer_offset + parameter.inputs[channel];
cmd.output = buffer_offset + parameter.outputs[channel];
cmd.biquad.b = parameter.b;
cmd.biquad.a = parameter.a;
cmd.state = memory_pool->Translate(CpuAddr(state),
MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState));
cmd.needs_init = needs_init;
cmd.use_float_processing = use_float_processing;
GenerateEnd<BiquadFilterCommand>(cmd);
}
void CommandBuffer::GenerateMixCommand(const s32 node_id, const s16 input_index,
const s16 output_index, const s16 buffer_offset,
const f32 volume, const u8 precision) {
auto& cmd{GenerateStart<MixCommand, CommandId::Mix>(node_id)};
cmd.input_index = input_index;
cmd.output_index = output_index;
cmd.volume = volume;
cmd.precision = precision;
GenerateEnd<MixCommand>(cmd);
}
void CommandBuffer::GenerateMixRampCommand(const s32 node_id,
[[maybe_unused]] const s16 buffer_count,
const s16 input_index, const s16 output_index,
const f32 volume, const f32 prev_volume,
const CpuAddr prev_samples, const u8 precision) {
if (volume == 0.0f && prev_volume == 0.0f) {
return;
}
auto& cmd{GenerateStart<MixRampCommand, CommandId::MixRamp>(node_id)};
cmd.input_index = input_index;
cmd.output_index = output_index;
cmd.prev_volume = prev_volume;
cmd.volume = volume;
cmd.previous_sample = prev_samples;
cmd.precision = precision;
GenerateEnd<MixRampCommand>(cmd);
}
void CommandBuffer::GenerateMixRampGroupedCommand(const s32 node_id, const s16 buffer_count,
const s16 input_index, s16 output_index,
std::span<const f32> volumes,
std::span<const f32> prev_volumes,
const CpuAddr prev_samples, const u8 precision) {
auto& cmd{GenerateStart<MixRampGroupedCommand, CommandId::MixRampGrouped>(node_id)};
cmd.buffer_count = buffer_count;
for (s32 i = 0; i < buffer_count; i++) {
cmd.inputs[i] = input_index;
cmd.outputs[i] = output_index++;
cmd.prev_volumes[i] = prev_volumes[i];
cmd.volumes[i] = volumes[i];
}
cmd.previous_samples = prev_samples;
cmd.precision = precision;
GenerateEnd<MixRampGroupedCommand>(cmd);
}
void CommandBuffer::GenerateDepopPrepareCommand(const s32 node_id, const VoiceState& voice_state,
std::span<const s32> buffer, const s16 buffer_count,
s16 buffer_offset, const bool was_playing) {
auto& cmd{GenerateStart<DepopPrepareCommand, CommandId::DepopPrepare>(node_id)};
cmd.enabled = was_playing;
for (u32 i = 0; i < MaxMixBuffers; i++) {
cmd.inputs[i] = buffer_offset++;
}
cmd.previous_samples = memory_pool->Translate(CpuAddr(voice_state.previous_samples.data()),
MaxMixBuffers * sizeof(s32));
cmd.buffer_count = buffer_count;
cmd.depop_buffer = memory_pool->Translate(CpuAddr(buffer.data()), buffer.size_bytes());
GenerateEnd<DepopPrepareCommand>(cmd);
}
void CommandBuffer::GenerateDepopForMixBuffersCommand(const s32 node_id, const MixInfo& mix_info,
std::span<const s32> depop_buffer) {
auto& cmd{GenerateStart<DepopForMixBuffersCommand, CommandId::DepopForMixBuffers>(node_id)};
cmd.input = mix_info.buffer_offset;
cmd.count = mix_info.buffer_count;
cmd.decay = mix_info.sample_rate == TargetSampleRate ? 0.96218872f : 0.94369507f;
cmd.depop_buffer =
memory_pool->Translate(CpuAddr(depop_buffer.data()), mix_info.buffer_count * sizeof(s32));
GenerateEnd<DepopForMixBuffersCommand>(cmd);
}
void CommandBuffer::GenerateDelayCommand(const s32 node_id, EffectInfoBase& effect_info,
const s16 buffer_offset) {
auto& cmd{GenerateStart<DelayCommand, CommandId::Delay>(node_id)};
const auto& parameter{
*reinterpret_cast<DelayInfo::ParameterVersion1*>(effect_info.GetParameter())};
const auto state{effect_info.GetStateBuffer()};
if (IsChannelCountValid(parameter.channel_count)) {
const auto state_buffer{memory_pool->Translate(CpuAddr(state), sizeof(DelayInfo::State))};
if (state_buffer) {
for (s16 channel = 0; channel < parameter.channel_count; channel++) {
cmd.inputs[channel] = buffer_offset + parameter.inputs[channel];
cmd.outputs[channel] = buffer_offset + parameter.outputs[channel];
}
if (!behavior->IsDelayChannelMappingChanged() && parameter.channel_count == 6) {
UseOldChannelMapping(cmd.inputs, cmd.outputs);
}
cmd.parameter = parameter;
cmd.effect_enabled = effect_info.IsEnabled();
cmd.state = state_buffer;
cmd.workbuffer = effect_info.GetWorkbuffer(-1);
}
}
GenerateEnd<DelayCommand>(cmd);
}
void CommandBuffer::GenerateUpsampleCommand(const s32 node_id, const s16 buffer_offset,
UpsamplerInfo& upsampler_info, const u32 input_count,
std::span<const s8> inputs, const s16 buffer_count,
const u32 sample_count_, const u32 sample_rate_) {
auto& cmd{GenerateStart<UpsampleCommand, CommandId::Upsample>(node_id)};
cmd.samples_buffer = memory_pool->Translate(upsampler_info.samples_pos,
upsampler_info.sample_count * sizeof(s32));
cmd.inputs = memory_pool->Translate(CpuAddr(upsampler_info.inputs.data()), MaxChannels);
cmd.buffer_count = buffer_count;
cmd.unk_20 = 0;
cmd.source_sample_count = sample_count_;
cmd.source_sample_rate = sample_rate_;
upsampler_info.input_count = input_count;
for (u32 i = 0; i < input_count; i++) {
upsampler_info.inputs[i] = buffer_offset + inputs[i];
}
cmd.upsampler_info = memory_pool->Translate(CpuAddr(&upsampler_info), sizeof(UpsamplerInfo));
GenerateEnd<UpsampleCommand>(cmd);
}
void CommandBuffer::GenerateDownMix6chTo2chCommand(const s32 node_id, std::span<const s8> inputs,
const s16 buffer_offset,
std::span<const f32> downmix_coeff) {
auto& cmd{GenerateStart<DownMix6chTo2chCommand, CommandId::DownMix6chTo2ch>(node_id)};
for (u32 i = 0; i < MaxChannels; i++) {
cmd.inputs[i] = buffer_offset + inputs[i];
cmd.outputs[i] = buffer_offset + inputs[i];
}
for (u32 i = 0; i < 4; i++) {
cmd.down_mix_coeff[i] = downmix_coeff[i];
}
GenerateEnd<DownMix6chTo2chCommand>(cmd);
}
void CommandBuffer::GenerateAuxCommand(const s32 node_id, EffectInfoBase& effect_info,
const s16 input_index, const s16 output_index,
const s16 buffer_offset, const u32 update_count,
const u32 count_max, const u32 write_offset) {
auto& cmd{GenerateStart<AuxCommand, CommandId::Aux>(node_id)};
if (effect_info.GetSendBuffer() != 0 && effect_info.GetReturnBuffer() != 0) {
cmd.input = buffer_offset + input_index;
cmd.output = buffer_offset + output_index;
cmd.send_buffer_info = effect_info.GetSendBufferInfo();
cmd.send_buffer = effect_info.GetSendBuffer();
cmd.return_buffer_info = effect_info.GetReturnBufferInfo();
cmd.return_buffer = effect_info.GetReturnBuffer();
cmd.count_max = count_max;
cmd.write_offset = write_offset;
cmd.update_count = update_count;
cmd.effect_enabled = effect_info.IsEnabled();
}
GenerateEnd<AuxCommand>(cmd);
}
void CommandBuffer::GenerateDeviceSinkCommand(const s32 node_id, const s16 buffer_offset,
SinkInfoBase& sink_info, const u32 session_id,
std::span<s32> samples_buffer) {
auto& cmd{GenerateStart<DeviceSinkCommand, CommandId::DeviceSink>(node_id)};
const auto& parameter{
*reinterpret_cast<DeviceSinkInfo::DeviceInParameter*>(sink_info.GetParameter())};
auto state{*reinterpret_cast<DeviceSinkInfo::DeviceState*>(sink_info.GetState())};
cmd.session_id = session_id;
cmd.input_count = parameter.input_count;
s16 max_input{0};
for (u32 i = 0; i < parameter.input_count; i++) {
cmd.inputs[i] = buffer_offset + parameter.inputs[i];
max_input = std::max(max_input, cmd.inputs[i]);
}
if (state.upsampler_info != nullptr) {
const auto size_{state.upsampler_info->sample_count * parameter.input_count};
const auto size_bytes{size_ * sizeof(s32)};
const auto addr{memory_pool->Translate(state.upsampler_info->samples_pos, size_bytes)};
cmd.sample_buffer = {reinterpret_cast<s32*>(addr),
(max_input + 1) * state.upsampler_info->sample_count};
} else {
cmd.sample_buffer = samples_buffer;
}
GenerateEnd<DeviceSinkCommand>(cmd);
}
void CommandBuffer::GenerateCircularBufferSinkCommand(const s32 node_id, SinkInfoBase& sink_info,
const s16 buffer_offset) {
auto& cmd{GenerateStart<CircularBufferSinkCommand, CommandId::CircularBufferSink>(node_id)};
const auto& parameter{*reinterpret_cast<CircularBufferSinkInfo::CircularBufferInParameter*>(
sink_info.GetParameter())};
auto state{
*reinterpret_cast<CircularBufferSinkInfo::CircularBufferState*>(sink_info.GetState())};
cmd.input_count = parameter.input_count;
for (u32 i = 0; i < parameter.input_count; i++) {
cmd.inputs[i] = buffer_offset + parameter.inputs[i];
}
cmd.address = state.address_info.GetReference(true);
cmd.size = parameter.size;
cmd.pos = state.current_pos;
GenerateEnd<CircularBufferSinkCommand>(cmd);
}
void CommandBuffer::GenerateReverbCommand(const s32 node_id, EffectInfoBase& effect_info,
const s16 buffer_offset,
const bool long_size_pre_delay_supported) {
auto& cmd{GenerateStart<ReverbCommand, CommandId::Reverb>(node_id)};
const auto& parameter{
*reinterpret_cast<ReverbInfo::ParameterVersion2*>(effect_info.GetParameter())};
const auto state{effect_info.GetStateBuffer()};
if (IsChannelCountValid(parameter.channel_count)) {
const auto state_buffer{memory_pool->Translate(CpuAddr(state), sizeof(ReverbInfo::State))};
if (state_buffer) {
for (s16 channel = 0; channel < parameter.channel_count; channel++) {
cmd.inputs[channel] = buffer_offset + parameter.inputs[channel];
cmd.outputs[channel] = buffer_offset + parameter.outputs[channel];
}
if (!behavior->IsReverbChannelMappingChanged() && parameter.channel_count == 6) {
UseOldChannelMapping(cmd.inputs, cmd.outputs);
}
cmd.parameter = parameter;
cmd.effect_enabled = effect_info.IsEnabled();
cmd.state = state_buffer;
cmd.workbuffer = effect_info.GetWorkbuffer(-1);
cmd.long_size_pre_delay_supported = long_size_pre_delay_supported;
}
}
GenerateEnd<ReverbCommand>(cmd);
}
void CommandBuffer::GenerateI3dl2ReverbCommand(const s32 node_id, EffectInfoBase& effect_info,
const s16 buffer_offset) {
auto& cmd{GenerateStart<I3dl2ReverbCommand, CommandId::I3dl2Reverb>(node_id)};
const auto& parameter{
*reinterpret_cast<I3dl2ReverbInfo::ParameterVersion1*>(effect_info.GetParameter())};
const auto state{effect_info.GetStateBuffer()};
if (IsChannelCountValid(parameter.channel_count)) {
const auto state_buffer{
memory_pool->Translate(CpuAddr(state), sizeof(I3dl2ReverbInfo::State))};
if (state_buffer) {
for (s16 channel = 0; channel < parameter.channel_count; channel++) {
cmd.inputs[channel] = buffer_offset + parameter.inputs[channel];
cmd.outputs[channel] = buffer_offset + parameter.outputs[channel];
}
if (!behavior->IsI3dl2ReverbChannelMappingChanged() && parameter.channel_count == 6) {
UseOldChannelMapping(cmd.inputs, cmd.outputs);
}
cmd.parameter = parameter;
cmd.effect_enabled = effect_info.IsEnabled();
cmd.state = state_buffer;
cmd.workbuffer = effect_info.GetWorkbuffer(-1);
}
}
GenerateEnd<I3dl2ReverbCommand>(cmd);
}
void CommandBuffer::GeneratePerformanceCommand(const s32 node_id, const PerformanceState state,
const PerformanceEntryAddresses& entry_addresses) {
auto& cmd{GenerateStart<PerformanceCommand, CommandId::Performance>(node_id)};
cmd.state = state;
cmd.entry_address = entry_addresses;
GenerateEnd<PerformanceCommand>(cmd);
}
void CommandBuffer::GenerateClearMixCommand(const s32 node_id) {
auto& cmd{GenerateStart<ClearMixBufferCommand, CommandId::ClearMixBuffer>(node_id)};
GenerateEnd<ClearMixBufferCommand>(cmd);
}
void CommandBuffer::GenerateCopyMixBufferCommand(const s32 node_id, EffectInfoBase& effect_info,
const s16 buffer_offset, const s8 channel) {
auto& cmd{GenerateStart<CopyMixBufferCommand, CommandId::CopyMixBuffer>(node_id)};
const auto& parameter{
*reinterpret_cast<BiquadFilterInfo::ParameterVersion1*>(effect_info.GetParameter())};
cmd.input_index = buffer_offset + parameter.inputs[channel];
cmd.output_index = buffer_offset + parameter.outputs[channel];
GenerateEnd<CopyMixBufferCommand>(cmd);
}
void CommandBuffer::GenerateLightLimiterCommand(
const s32 node_id, const s16 buffer_offset,
const LightLimiterInfo::ParameterVersion1& parameter, const LightLimiterInfo::State& state,
const bool enabled, const CpuAddr workbuffer) {
auto& cmd{GenerateStart<LightLimiterVersion1Command, CommandId::LightLimiterVersion1>(node_id)};
if (IsChannelCountValid(parameter.channel_count)) {
const auto state_buffer{
memory_pool->Translate(CpuAddr(&state), sizeof(LightLimiterInfo::State))};
if (state_buffer) {
for (s8 channel = 0; channel < parameter.channel_count; channel++) {
cmd.inputs[channel] = buffer_offset + parameter.inputs[channel];
cmd.outputs[channel] = buffer_offset + parameter.outputs[channel];
}
std::memcpy(&cmd.parameter, &parameter, sizeof(LightLimiterInfo::ParameterVersion1));
cmd.effect_enabled = enabled;
cmd.state = state_buffer;
cmd.workbuffer = workbuffer;
}
}
GenerateEnd<LightLimiterVersion1Command>(cmd);
}
void CommandBuffer::GenerateLightLimiterCommand(
const s32 node_id, const s16 buffer_offset,
const LightLimiterInfo::ParameterVersion2& parameter,
const LightLimiterInfo::StatisticsInternal& statistics, const LightLimiterInfo::State& state,
const bool enabled, const CpuAddr workbuffer) {
auto& cmd{GenerateStart<LightLimiterVersion2Command, CommandId::LightLimiterVersion2>(node_id)};
if (IsChannelCountValid(parameter.channel_count)) {
const auto state_buffer{
memory_pool->Translate(CpuAddr(&state), sizeof(LightLimiterInfo::State))};
if (state_buffer) {
for (s8 channel = 0; channel < parameter.channel_count; channel++) {
cmd.inputs[channel] = buffer_offset + parameter.inputs[channel];
cmd.outputs[channel] = buffer_offset + parameter.outputs[channel];
}
cmd.parameter = parameter;
cmd.effect_enabled = enabled;
cmd.state = state_buffer;
if (cmd.parameter.statistics_enabled) {
cmd.result_state = memory_pool->Translate(
CpuAddr(&statistics), sizeof(LightLimiterInfo::StatisticsInternal));
} else {
cmd.result_state = 0;
}
cmd.workbuffer = workbuffer;
}
}
GenerateEnd<LightLimiterVersion2Command>(cmd);
}
void CommandBuffer::GenerateMultitapBiquadFilterCommand(const s32 node_id, VoiceInfo& voice_info,
const VoiceState& voice_state,
const s16 buffer_count, const s8 channel) {
auto& cmd{GenerateStart<MultiTapBiquadFilterCommand, CommandId::MultiTapBiquadFilter>(node_id)};
cmd.input = buffer_count + channel;
cmd.output = buffer_count + channel;
cmd.biquads = voice_info.biquads;
cmd.states[0] =
memory_pool->Translate(CpuAddr(voice_state.biquad_states[0].data()),
MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState));
cmd.states[1] =
memory_pool->Translate(CpuAddr(voice_state.biquad_states[1].data()),
MaxBiquadFilters * sizeof(VoiceState::BiquadFilterState));
cmd.needs_init[0] = !voice_info.biquad_initialized[0];
cmd.needs_init[1] = !voice_info.biquad_initialized[1];
cmd.filter_tap_count = MaxBiquadFilters;
GenerateEnd<MultiTapBiquadFilterCommand>(cmd);
}
void CommandBuffer::GenerateCaptureCommand(const s32 node_id, EffectInfoBase& effect_info,
const s16 input_index, const s16 output_index,
const s16 buffer_offset, const u32 update_count,
const u32 count_max, const u32 write_offset) {
auto& cmd{GenerateStart<CaptureCommand, CommandId::Capture>(node_id)};
if (effect_info.GetSendBuffer()) {
cmd.input = buffer_offset + input_index;
cmd.output = buffer_offset + output_index;
cmd.send_buffer_info = effect_info.GetSendBufferInfo();
cmd.send_buffer = effect_info.GetSendBuffer();
cmd.count_max = count_max;
cmd.write_offset = write_offset;
cmd.update_count = update_count;
cmd.effect_enabled = effect_info.IsEnabled();
}
GenerateEnd<CaptureCommand>(cmd);
}
void CommandBuffer::GenerateCompressorCommand(s16 buffer_offset, EffectInfoBase& effect_info,
s32 node_id) {
auto& cmd{GenerateStart<CompressorCommand, CommandId::Compressor>(node_id)};
auto& parameter{
*reinterpret_cast<CompressorInfo::ParameterVersion2*>(effect_info.GetParameter())};
auto state{reinterpret_cast<CompressorInfo::State*>(effect_info.GetStateBuffer())};
if (IsChannelCountValid(parameter.channel_count)) {
auto state_buffer{memory_pool->Translate(CpuAddr(state), sizeof(CompressorInfo::State))};
if (state_buffer) {
for (u16 channel = 0; channel < parameter.channel_count; channel++) {
cmd.inputs[channel] = buffer_offset + parameter.inputs[channel];
cmd.outputs[channel] = buffer_offset + parameter.outputs[channel];
}
cmd.parameter = parameter;
cmd.workbuffer = state_buffer;
cmd.enabled = effect_info.IsEnabled();
}
}
GenerateEnd<CompressorCommand>(cmd);
}
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,468 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <span>
#include "audio_core/renderer/command/commands.h"
#include "audio_core/renderer/effect/light_limiter.h"
#include "audio_core/renderer/performance/performance_manager.h"
#include "common/common_types.h"
namespace AudioCore::Renderer {
struct UpsamplerInfo;
struct VoiceState;
class EffectInfoBase;
class ICommandProcessingTimeEstimator;
class MixInfo;
class MemoryPoolInfo;
class SinkInfoBase;
class VoiceInfo;
/**
* Utility functions to generate and add commands into the current command list.
*/
class CommandBuffer {
public:
/**
* Generate a PCM s16 version 1 command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param memory_pool - Memory pool for translating buffer addresses to the DSP.
* @param voice_info - The voice info this command is generated from.
* @param voice_state - The voice state the DSP will use for this command.
* @param buffer_count - Number of mix buffers in use,
* data will be read into this index + channel.
* @param channel - Channel index for this command.
*/
void GeneratePcmInt16Version1Command(s32 node_id, const MemoryPoolInfo& memory_pool,
VoiceInfo& voice_info, const VoiceState& voice_state,
s16 buffer_count, s8 channel);
/**
* Generate a PCM s16 version 2 command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param voice_info - The voice info this command is generated from.
* @param voice_state - The voice state the DSP will use for this command.
* @param buffer_count - Number of mix buffers in use,
* data will be read into this index + channel.
* @param channel - Channel index for this command.
*/
void GeneratePcmInt16Version2Command(s32 node_id, VoiceInfo& voice_info,
const VoiceState& voice_state, s16 buffer_count,
s8 channel);
/**
* Generate a PCM f32 version 1 command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param memory_pool - Memory pool for translating buffer addresses to the DSP.
* @param voice_info - The voice info this command is generated from.
* @param voice_state - The voice state the DSP will use for this command.
* @param buffer_count - Number of mix buffers in use,
* data will be read into this index + channel.
* @param channel - Channel index for this command.
*/
void GeneratePcmFloatVersion1Command(s32 node_id, const MemoryPoolInfo& memory_pool,
VoiceInfo& voice_info, const VoiceState& voice_state,
s16 buffer_count, s8 channel);
/**
* Generate a PCM f32 version 2 command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param voice_info - The voice info this command is generated from.
* @param voice_state - The voice state the DSP will use for this command.
* @param buffer_count - Number of mix buffers in use,
* data will be read into this index + channel.
* @param channel - Channel index for this command.
*/
void GeneratePcmFloatVersion2Command(s32 node_id, VoiceInfo& voice_info,
const VoiceState& voice_state, s16 buffer_count,
s8 channel);
/**
* Generate an ADPCM version 1 command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param memory_pool - Memory pool for translating buffer addresses to the DSP.
* @param voice_info - The voice info this command is generated from.
* @param voice_state - The voice state the DSP will use for this command.
* @param buffer_count - Number of mix buffers in use,
* data will be read into this index + channel.
* @param channel - Channel index for this command.
*/
void GenerateAdpcmVersion1Command(s32 node_id, const MemoryPoolInfo& memory_pool,
VoiceInfo& voice_info, const VoiceState& voice_state,
s16 buffer_count, s8 channel);
/**
* Generate an ADPCM version 2 command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param voice_info - The voice info this command is generated from.
* @param voice_state - The voice state the DSP will use for this command.
* @param buffer_count - Number of mix buffers in use,
* data will be read into this index + channel.
* @param channel - Channel index for this command.
*/
void GenerateAdpcmVersion2Command(s32 node_id, VoiceInfo& voice_info,
const VoiceState& voice_state, s16 buffer_count, s8 channel);
/**
* Generate a volume command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param buffer_offset - Base mix buffer index to generate this command at.
* @param input_index - Channel index and mix buffer offset for this command.
* @param volume - Mix volume added to the input samples.
* @param precision - Number of decimal bits for fixed point operations.
*/
void GenerateVolumeCommand(s32 node_id, s16 buffer_offset, s16 input_index, f32 volume,
u8 precision);
/**
* Generate a volume ramp command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param voice_info - The voice info this command takes its volumes from.
* @param buffer_count - Number of active mix buffers, command will generate at this index.
* @param precision - Number of decimal bits for fixed point operations.
*/
void GenerateVolumeRampCommand(s32 node_id, VoiceInfo& voice_info, s16 buffer_count,
u8 precision);
/**
* Generate a biquad filter command from a voice, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param voice_info - The voice info this command takes biquad parameters from.
* @param voice_state - Used by the AudioRenderer to track previous samples.
* @param buffer_count - Number of active mix buffers,
* command will generate at this index + channel.
* @param channel - Channel index for this filter to work on.
* @param biquad_index - Which biquad filter to use for this command (0-1).
* @param use_float_processing - Should int or float processing be used?
*/
void GenerateBiquadFilterCommand(s32 node_id, VoiceInfo& voice_info,
const VoiceState& voice_state, s16 buffer_count, s8 channel,
u32 biquad_index, bool use_float_processing);
/**
* Generate a biquad filter effect command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param effect_info - The effect info this command takes biquad parameters from.
* @param buffer_offset - Mix buffer offset this command will use,
* command will generate at this index + channel.
* @param channel - Channel index for this filter to work on.
* @param needs_init - True if the biquad state needs initialisation.
* @param use_float_processing - Should int or float processing be used?
*/
void GenerateBiquadFilterCommand(s32 node_id, EffectInfoBase& effect_info, s16 buffer_offset,
s8 channel, bool needs_init, bool use_float_processing);
/**
* Generate a mix command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param input_index - Input mix buffer index for this command.
* Added to the buffer offset.
* @param output_index - Output mix buffer index for this command.
* Added to the buffer offset.
* @param buffer_offset - Mix buffer offset this command will use.
* @param volume - Volume to be applied to the input.
* @param precision - Number of decimal bits for fixed point operations.
*/
void GenerateMixCommand(s32 node_id, s16 input_index, s16 output_index, s16 buffer_offset,
f32 volume, u8 precision);
/**
* Generate a mix ramp command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param buffer_count - Number of active mix buffers.
* @param input_index - Input mix buffer index for this command.
* Added to buffer_count.
* @param output_index - Output mix buffer index for this command.
* Added to buffer_count.
* @param volume - Current mix volume used for calculating the ramp.
* @param prev_volume - Previous mix volume, used for calculating the ramp,
* also applied to the input.
* @param prev_samples - Previous sample buffer. Used for depopping.
* @param precision - Number of decimal bits for fixed point operations.
*/
void GenerateMixRampCommand(s32 node_id, s16 buffer_count, s16 input_index, s16 output_index,
f32 volume, f32 prev_volume, CpuAddr prev_samples, u8 precision);
/**
* Generate a mix ramp grouped command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param buffer_count - Number of active mix buffers.
* @param input_index - Input mix buffer index for this command.
* Added to buffer_count.
* @param output_index - Output mix buffer index for this command.
* Added to buffer_count.
* @param volumes - Current mix volumes used for calculating the ramp.
* @param prev_volumes - Previous mix volumes, used for calculating the ramp,
* also applied to the input.
* @param prev_samples - Previous sample buffer. Used for depopping.
* @param precision - Number of decimal bits for fixed point operations.
*/
void GenerateMixRampGroupedCommand(s32 node_id, s16 buffer_count, s16 input_index,
s16 output_index, std::span<const f32> volumes,
std::span<const f32> prev_volumes, CpuAddr prev_samples,
u8 precision);
/**
* Generate a depop prepare command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param voice_state - State to track the previous depop samples for each mix buffer.
* @param buffer - State to track the current depop samples for each mix buffer.
* @param buffer_count - Number of active mix buffers.
* @param buffer_offset - Base mix buffer index to generate the channel depops at.
* @param was_playing - Command only needs to work if the voice was previously playing.
*/
void GenerateDepopPrepareCommand(s32 node_id, const VoiceState& voice_state,
std::span<const s32> buffer, s16 buffer_count,
s16 buffer_offset, bool was_playing);
/**
* Generate a depop command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param mix_info - Mix info to get the buffer count and base offsets from.
* @param depop_buffer - Buffer of current depop sample values to be added to the input
* channels.
*/
void GenerateDepopForMixBuffersCommand(s32 node_id, const MixInfo& mix_info,
std::span<const s32> depop_buffer);
/**
* Generate a delay command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param effect_info - Delay effect info to generate this command from.
* @param buffer_offset - Base mix buffer offset to apply the apply the delay.
*/
void GenerateDelayCommand(s32 node_id, EffectInfoBase& effect_info, s16 buffer_offset);
/**
* Generate an upsample command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param buffer_offset - Base mix buffer offset to upsample.
* @param upsampler_info - Upsampler info to control the upsampling.
* @param input_count - Number of input channels to upsample.
* @param inputs - Input mix buffer indexes.
* @param buffer_count - Number of active mix buffers.
* @param sample_count - Source sample count of the input.
* @param sample_rate - Source sample rate of the input.
*/
void GenerateUpsampleCommand(s32 node_id, s16 buffer_offset, UpsamplerInfo& upsampler_info,
u32 input_count, std::span<const s8> inputs, s16 buffer_count,
u32 sample_count, u32 sample_rate);
/**
* Generate a downmix 6 -> 2 command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param inputs - Input mix buffer indexes.
* @param buffer_offset - Base mix buffer offset of the channels to downmix.
* @param downmix_coeff - Downmixing coefficients.
*/
void GenerateDownMix6chTo2chCommand(s32 node_id, std::span<const s8> inputs, s16 buffer_offset,
std::span<const f32> downmix_coeff);
/**
* Generate an aux buffer command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param effect_info - Aux effect info to generate this command from.
* @param input_index - Input mix buffer index for this command.
* Added to buffer_offset.
* @param output_index - Output mix buffer index for this command.
* Added to buffer_offset.
* @param buffer_offset - Base mix buffer offset to use.
* @param update_count - Number of samples to write back to the game as updated, can be 0.
* @param count_max - Maximum number of samples to read or write.
* @param write_offset - Current read or write offset within the buffer.
*/
void GenerateAuxCommand(s32 node_id, EffectInfoBase& effect_info, s16 input_index,
s16 output_index, s16 buffer_offset, u32 update_count, u32 count_max,
u32 write_offset);
/**
* Generate a device sink command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param buffer_offset - Base mix buffer offset to use.
* @param sink_info - The sink_info to generate this command from.
* @param session_id - System session id this command is generated from.
* @param samples_buffer - The buffer to be sent to the sink if upsampling is not used.
*/
void GenerateDeviceSinkCommand(s32 node_id, s16 buffer_offset, SinkInfoBase& sink_info,
u32 session_id, std::span<s32> samples_buffer);
/**
* Generate a circular buffer sink command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param sink_info - The sink_info to generate this command from.
* @param buffer_offset - Base mix buffer offset to use.
*/
void GenerateCircularBufferSinkCommand(s32 node_id, SinkInfoBase& sink_info, s16 buffer_offset);
/**
* Generate a reverb command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param effect_info - Reverb effect info to generate this command from.
* @param buffer_offset - Base mix buffer offset to use.
* @param long_size_pre_delay_supported - Should a longer pre-delay time be used before reverb
* begins?
*/
void GenerateReverbCommand(s32 node_id, EffectInfoBase& effect_info, s16 buffer_offset,
bool long_size_pre_delay_supported);
/**
* Generate an I3DL2 reverb command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param effect_info - I3DL2Reverb effect info to generate this command from.
* @param buffer_offset - Base mix buffer offset to use.
*/
void GenerateI3dl2ReverbCommand(s32 node_id, EffectInfoBase& effect_info, s16 buffer_offset);
/**
* Generate a performance command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param state - State of the performance.
* @param entry_addresses - The addresses to be filled in by the AudioRenderer.
*/
void GeneratePerformanceCommand(s32 node_id, PerformanceState state,
const PerformanceEntryAddresses& entry_addresses);
/**
* Generate a clear mix command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
*/
void GenerateClearMixCommand(s32 node_id);
/**
* Generate a copy mix command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param effect_info - BiquadFilter effect info to generate this command from.
* @param buffer_offset - Base mix buffer offset to use.
* @param channel - Index to the effect's parameters input indexes for this command.
*/
void GenerateCopyMixBufferCommand(s32 node_id, EffectInfoBase& effect_info, s16 buffer_offset,
s8 channel);
/**
* Generate a light limiter version 1 command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param buffer_offset - Base mix buffer offset to use.
* @param parameter - Effect parameter to generate from.
* @param state - State used by the AudioRenderer between commands.
* @param enabled - Is this command enabled?
* @param workbuffer - Game-supplied memory for the state.
*/
void GenerateLightLimiterCommand(s32 node_id, s16 buffer_offset,
const LightLimiterInfo::ParameterVersion1& parameter,
const LightLimiterInfo::State& state, bool enabled,
CpuAddr workbuffer);
/**
* Generate a light limiter version 2 command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param buffer_offset - Base mix buffer offset to use.
* @param parameter - Effect parameter to generate from.
* @param statistics - Statistics reported by the AudioRenderer on the limiter's state.
* @param state - State used by the AudioRenderer between commands.
* @param enabled - Is this command enabled?
* @param workbuffer - Game-supplied memory for the state.
*/
void GenerateLightLimiterCommand(s32 node_id, s16 buffer_offset,
const LightLimiterInfo::ParameterVersion2& parameter,
const LightLimiterInfo::StatisticsInternal& statistics,
const LightLimiterInfo::State& state, bool enabled,
CpuAddr workbuffer);
/**
* Generate a multitap biquad filter command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param voice_info - The voice info this command takes biquad parameters from.
* @param voice_state - Used by the AudioRenderer to track previous samples.
* @param buffer_count - Number of active mix buffers,
* command will generate at this index + channel.
* @param channel - Channel index for this filter to work on.
*/
void GenerateMultitapBiquadFilterCommand(s32 node_id, VoiceInfo& voice_info,
const VoiceState& voice_state, s16 buffer_count,
s8 channel);
/**
* Generate a capture command, adding it to the command list.
*
* @param node_id - Node id of the voice this command is generated for.
* @param effect_info - Capture effect info to generate this command from.
* @param input_index - Input mix buffer index for this command.
* Added to buffer_offset.
* @param output_index - Output mix buffer index for this command (unused).
* Added to buffer_offset.
* @param buffer_offset - Base mix buffer offset to use.
* @param update_count - Number of samples to write back to the game as updated, can be 0.
* @param count_max - Maximum number of samples to read or write.
* @param write_offset - Current read or write offset within the buffer.
*/
void GenerateCaptureCommand(s32 node_id, EffectInfoBase& effect_info, s16 input_index,
s16 output_index, s16 buffer_offset, u32 update_count,
u32 count_max, u32 write_offset);
/**
* Generate a compressor command, adding it to the command list.
*
* @param buffer_offset - Base mix buffer offset to use.
* @param effect_info - Capture effect info to generate this command from.
* @param node_id - Node id of the voice this command is generated for.
*/
void GenerateCompressorCommand(s16 buffer_offset, EffectInfoBase& effect_info, s32 node_id);
/// Command list buffer generated commands will be added to
std::span<u8> command_list{};
/// Input sample count, unused
u32 sample_count{};
/// Input sample rate, unused
u32 sample_rate{};
/// Current size of the command buffer
u64 size{};
/// Current number of commands added
u32 count{};
/// Current estimated processing time for all commands
u32 estimated_process_time{};
/// Used for mapping buffers for the AudioRenderer
MemoryPoolInfo* memory_pool{};
/// Used for estimating command process times
ICommandProcessingTimeEstimator* time_estimator{};
/// Used to check which rendering features are currently enabled
BehaviorInfo* behavior{};
private:
template <typename T, CommandId Id>
T& GenerateStart(const s32 node_id);
template <typename T>
void GenerateEnd(T& cmd);
};
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,796 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/common/audio_renderer_parameter.h"
#include "audio_core/renderer/behavior/behavior_info.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/aux_.h"
#include "audio_core/renderer/effect/biquad_filter.h"
#include "audio_core/renderer/effect/buffer_mixer.h"
#include "audio_core/renderer/effect/capture.h"
#include "audio_core/renderer/effect/effect_context.h"
#include "audio_core/renderer/effect/light_limiter.h"
#include "audio_core/renderer/mix/mix_context.h"
#include "audio_core/renderer/performance/detail_aspect.h"
#include "audio_core/renderer/performance/entry_aspect.h"
#include "audio_core/renderer/sink/device_sink_info.h"
#include "audio_core/renderer/sink/sink_context.h"
#include "audio_core/renderer/splitter/splitter_context.h"
#include "audio_core/renderer/voice/voice_context.h"
#include "common/alignment.h"
namespace AudioCore::Renderer {
CommandGenerator::CommandGenerator(CommandBuffer& command_buffer_,
const CommandListHeader& command_list_header_,
const AudioRendererSystemContext& render_context_,
VoiceContext& voice_context_, MixContext& mix_context_,
EffectContext& effect_context_, SinkContext& sink_context_,
SplitterContext& splitter_context_,
PerformanceManager* performance_manager_)
: command_buffer{command_buffer_}, command_header{command_list_header_},
render_context{render_context_}, voice_context{voice_context_}, mix_context{mix_context_},
effect_context{effect_context_}, sink_context{sink_context_},
splitter_context{splitter_context_}, performance_manager{performance_manager_} {
command_buffer.GenerateClearMixCommand(InvalidNodeId);
}
void CommandGenerator::GenerateDataSourceCommand(VoiceInfo& voice_info,
const VoiceState& voice_state, const s8 channel) {
if (voice_info.mix_id == UnusedMixId) {
if (voice_info.splitter_id != UnusedSplitterId) {
auto destination{splitter_context.GetDestinationData(voice_info.splitter_id, 0)};
u32 dest_id{0};
while (destination != nullptr) {
if (destination->IsConfigured()) {
auto mix_id{destination->GetMixId()};
if (mix_id < mix_context.GetCount() && mix_id != UnusedSplitterId) {
auto mix_info{mix_context.GetInfo(mix_id)};
command_buffer.GenerateDepopPrepareCommand(
voice_info.node_id, voice_state, render_context.depop_buffer,
mix_info->buffer_count, mix_info->buffer_offset,
voice_info.was_playing);
}
}
dest_id++;
destination = splitter_context.GetDestinationData(voice_info.splitter_id, dest_id);
}
}
} else {
auto mix_info{mix_context.GetInfo(voice_info.mix_id)};
command_buffer.GenerateDepopPrepareCommand(
voice_info.node_id, voice_state, render_context.depop_buffer, mix_info->buffer_count,
mix_info->buffer_offset, voice_info.was_playing);
}
if (voice_info.was_playing) {
return;
}
if (render_context.behavior->IsWaveBufferVer2Supported()) {
switch (voice_info.sample_format) {
case SampleFormat::PcmInt16:
command_buffer.GeneratePcmInt16Version2Command(
voice_info.node_id, voice_info, voice_state, render_context.mix_buffer_count,
channel);
break;
case SampleFormat::PcmFloat:
command_buffer.GeneratePcmFloatVersion2Command(
voice_info.node_id, voice_info, voice_state, render_context.mix_buffer_count,
channel);
break;
case SampleFormat::Adpcm:
command_buffer.GenerateAdpcmVersion2Command(voice_info.node_id, voice_info, voice_state,
render_context.mix_buffer_count, channel);
break;
default:
LOG_ERROR(Service_Audio, "Invalid SampleFormat {}",
static_cast<u32>(voice_info.sample_format));
break;
}
} else {
switch (voice_info.sample_format) {
case SampleFormat::PcmInt16:
command_buffer.GeneratePcmInt16Version1Command(
voice_info.node_id, *command_buffer.memory_pool, voice_info, voice_state,
render_context.mix_buffer_count, channel);
break;
case SampleFormat::PcmFloat:
command_buffer.GeneratePcmFloatVersion1Command(
voice_info.node_id, *command_buffer.memory_pool, voice_info, voice_state,
render_context.mix_buffer_count, channel);
break;
case SampleFormat::Adpcm:
command_buffer.GenerateAdpcmVersion1Command(
voice_info.node_id, *command_buffer.memory_pool, voice_info, voice_state,
render_context.mix_buffer_count, channel);
break;
default:
LOG_ERROR(Service_Audio, "Invalid SampleFormat {}",
static_cast<u32>(voice_info.sample_format));
break;
}
}
}
void CommandGenerator::GenerateVoiceMixCommand(std::span<const f32> mix_volumes,
std::span<const f32> prev_mix_volumes,
const VoiceState& voice_state, s16 output_index,
const s16 buffer_count, const s16 input_index,
const s32 node_id) {
u8 precision{15};
if (render_context.behavior->IsVolumeMixParameterPrecisionQ23Supported()) {
precision = 23;
}
if (buffer_count > 8) {
const auto prev_samples{render_context.memory_pool_info->Translate(
CpuAddr(voice_state.previous_samples.data()), buffer_count * sizeof(s32))};
command_buffer.GenerateMixRampGroupedCommand(node_id, buffer_count, input_index,
output_index, mix_volumes, prev_mix_volumes,
prev_samples, precision);
} else {
for (s16 i = 0; i < buffer_count; i++, output_index++) {
const auto prev_samples{render_context.memory_pool_info->Translate(
CpuAddr(&voice_state.previous_samples[i]), sizeof(s32))};
command_buffer.GenerateMixRampCommand(node_id, buffer_count, input_index, output_index,
mix_volumes[i], prev_mix_volumes[i], prev_samples,
precision);
}
}
}
void CommandGenerator::GenerateBiquadFilterCommandForVoice(VoiceInfo& voice_info,
const VoiceState& voice_state,
const s16 buffer_count, const s8 channel,
const s32 node_id) {
const bool both_biquads_enabled{voice_info.biquads[0].enabled && voice_info.biquads[1].enabled};
const auto use_float_processing{render_context.behavior->UseBiquadFilterFloatProcessing()};
if (both_biquads_enabled && render_context.behavior->UseMultiTapBiquadFilterProcessing() &&
use_float_processing) {
command_buffer.GenerateMultitapBiquadFilterCommand(node_id, voice_info, voice_state,
buffer_count, channel);
} else {
for (u32 i = 0; i < MaxBiquadFilters; i++) {
if (voice_info.biquads[i].enabled) {
command_buffer.GenerateBiquadFilterCommand(node_id, voice_info, voice_state,
buffer_count, channel, i,
use_float_processing);
}
}
}
}
void CommandGenerator::GenerateVoiceCommand(VoiceInfo& voice_info) {
u8 precision{15};
if (render_context.behavior->IsVolumeMixParameterPrecisionQ23Supported()) {
precision = 23;
}
for (s8 channel = 0; channel < voice_info.channel_count; channel++) {
const auto resource_id{voice_info.channel_resource_ids[channel]};
auto& voice_state{voice_context.GetDspSharedState(resource_id)};
auto& channel_resource{voice_context.GetChannelResource(resource_id)};
PerformanceDetailType detail_type{PerformanceDetailType::Invalid};
switch (voice_info.sample_format) {
case SampleFormat::PcmInt16:
detail_type = PerformanceDetailType::Unk1;
break;
case SampleFormat::PcmFloat:
detail_type = PerformanceDetailType::Unk10;
break;
default:
detail_type = PerformanceDetailType::Unk2;
break;
}
DetailAspect data_source_detail(*this, PerformanceEntryType::Voice, voice_info.node_id,
detail_type);
GenerateDataSourceCommand(voice_info, voice_state, channel);
if (data_source_detail.initialized) {
command_buffer.GeneratePerformanceCommand(data_source_detail.node_id,
PerformanceState::Stop,
data_source_detail.performance_entry_address);
}
if (voice_info.was_playing) {
voice_info.prev_volume = 0.0f;
continue;
}
if (!voice_info.HasAnyConnection()) {
continue;
}
DetailAspect biquad_detail_aspect(*this, PerformanceEntryType::Voice, voice_info.node_id,
PerformanceDetailType::Unk4);
GenerateBiquadFilterCommandForVoice(
voice_info, voice_state, render_context.mix_buffer_count, channel, voice_info.node_id);
if (biquad_detail_aspect.initialized) {
command_buffer.GeneratePerformanceCommand(
biquad_detail_aspect.node_id, PerformanceState::Stop,
biquad_detail_aspect.performance_entry_address);
}
DetailAspect volume_ramp_detail_aspect(*this, PerformanceEntryType::Voice,
voice_info.node_id, PerformanceDetailType::Unk3);
command_buffer.GenerateVolumeRampCommand(
voice_info.node_id, voice_info, render_context.mix_buffer_count + channel, precision);
if (volume_ramp_detail_aspect.initialized) {
command_buffer.GeneratePerformanceCommand(
volume_ramp_detail_aspect.node_id, PerformanceState::Stop,
volume_ramp_detail_aspect.performance_entry_address);
}
voice_info.prev_volume = voice_info.volume;
if (voice_info.mix_id == UnusedMixId) {
if (voice_info.splitter_id != UnusedSplitterId) {
auto i{channel};
auto destination{splitter_context.GetDestinationData(voice_info.splitter_id, i)};
while (destination != nullptr) {
if (destination->IsConfigured()) {
const auto mix_id{destination->GetMixId()};
if (mix_id < mix_context.GetCount() &&
static_cast<s32>(mix_id) != UnusedSplitterId) {
auto mix_info{mix_context.GetInfo(mix_id)};
GenerateVoiceMixCommand(
destination->GetMixVolume(), destination->GetMixVolumePrev(),
voice_state, mix_info->buffer_offset, mix_info->buffer_count,
render_context.mix_buffer_count + channel, voice_info.node_id);
destination->MarkAsNeedToUpdateInternalState();
}
}
i += voice_info.channel_count;
destination = splitter_context.GetDestinationData(voice_info.splitter_id, i);
}
}
} else {
DetailAspect volume_mix_detail_aspect(*this, PerformanceEntryType::Voice,
voice_info.node_id, PerformanceDetailType::Unk3);
auto mix_info{mix_context.GetInfo(voice_info.mix_id)};
GenerateVoiceMixCommand(channel_resource.mix_volumes, channel_resource.prev_mix_volumes,
voice_state, mix_info->buffer_offset, mix_info->buffer_count,
render_context.mix_buffer_count + channel, voice_info.node_id);
if (volume_mix_detail_aspect.initialized) {
command_buffer.GeneratePerformanceCommand(
volume_mix_detail_aspect.node_id, PerformanceState::Stop,
volume_mix_detail_aspect.performance_entry_address);
}
channel_resource.prev_mix_volumes = channel_resource.mix_volumes;
}
voice_info.biquad_initialized[0] = voice_info.biquads[0].enabled;
voice_info.biquad_initialized[1] = voice_info.biquads[1].enabled;
}
}
void CommandGenerator::GenerateVoiceCommands() {
const auto voice_count{voice_context.GetCount()};
for (u32 i = 0; i < voice_count; i++) {
auto sorted_info{voice_context.GetSortedInfo(i)};
if (sorted_info->ShouldSkip() || !sorted_info->UpdateForCommandGeneration(voice_context)) {
continue;
}
EntryAspect voice_entry_aspect(*this, PerformanceEntryType::Voice, sorted_info->node_id);
GenerateVoiceCommand(*sorted_info);
if (voice_entry_aspect.initialized) {
command_buffer.GeneratePerformanceCommand(voice_entry_aspect.node_id,
PerformanceState::Stop,
voice_entry_aspect.performance_entry_address);
}
}
splitter_context.UpdateInternalState();
}
void CommandGenerator::GenerateBufferMixerCommand(const s16 buffer_offset,
EffectInfoBase& effect_info, const s32 node_id) {
u8 precision{15};
if (render_context.behavior->IsVolumeMixParameterPrecisionQ23Supported()) {
precision = 23;
}
if (effect_info.IsEnabled()) {
const auto& parameter{
*reinterpret_cast<BufferMixerInfo::ParameterVersion1*>(effect_info.GetParameter())};
for (u32 i = 0; i < parameter.mix_count; i++) {
if (parameter.volumes[i] != 0.0f) {
command_buffer.GenerateMixCommand(node_id, buffer_offset + parameter.inputs[i],
buffer_offset + parameter.outputs[i],
buffer_offset, parameter.volumes[i], precision);
}
}
}
}
void CommandGenerator::GenerateDelayCommand(const s16 buffer_offset, EffectInfoBase& effect_info,
const s32 node_id) {
command_buffer.GenerateDelayCommand(node_id, effect_info, buffer_offset);
}
void CommandGenerator::GenerateReverbCommand(const s16 buffer_offset, EffectInfoBase& effect_info,
const s32 node_id,
const bool long_size_pre_delay_supported) {
command_buffer.GenerateReverbCommand(node_id, effect_info, buffer_offset,
long_size_pre_delay_supported);
}
void CommandGenerator::GenerateI3dl2ReverbEffectCommand(const s16 buffer_offset,
EffectInfoBase& effect_info,
const s32 node_id) {
command_buffer.GenerateI3dl2ReverbCommand(node_id, effect_info, buffer_offset);
}
void CommandGenerator::GenerateAuxCommand(const s16 buffer_offset, EffectInfoBase& effect_info,
const s32 node_id) {
if (effect_info.IsEnabled()) {
effect_info.GetWorkbuffer(0);
effect_info.GetWorkbuffer(1);
}
if (effect_info.GetSendBuffer() != 0 && effect_info.GetReturnBuffer() != 0) {
const auto& parameter{
*reinterpret_cast<AuxInfo::ParameterVersion1*>(effect_info.GetParameter())};
auto channel_index{parameter.mix_buffer_count - 1};
u32 write_offset{0};
for (u32 i = 0; i < parameter.mix_buffer_count; i++, channel_index--) {
auto new_update_count{command_header.sample_count + write_offset};
const auto update_count{channel_index > 0 ? 0 : new_update_count};
command_buffer.GenerateAuxCommand(node_id, effect_info, parameter.inputs[i],
parameter.outputs[i], buffer_offset, update_count,
parameter.count_max, write_offset);
write_offset = new_update_count;
}
}
}
void CommandGenerator::GenerateBiquadFilterEffectCommand(const s16 buffer_offset,
EffectInfoBase& effect_info,
const s32 node_id) {
const auto& parameter{
*reinterpret_cast<BiquadFilterInfo::ParameterVersion1*>(effect_info.GetParameter())};
if (effect_info.IsEnabled()) {
bool needs_init{false};
switch (parameter.state) {
case EffectInfoBase::ParameterState::Initialized:
needs_init = true;
break;
case EffectInfoBase::ParameterState::Updating:
case EffectInfoBase::ParameterState::Updated:
if (render_context.behavior->IsBiquadFilterEffectStateClearBugFixed()) {
needs_init = false;
} else {
needs_init = parameter.state == EffectInfoBase::ParameterState::Updating;
}
break;
default:
LOG_ERROR(Service_Audio, "Invalid biquad parameter state {}",
static_cast<u32>(parameter.state));
break;
}
for (s8 channel = 0; channel < parameter.channel_count; channel++) {
command_buffer.GenerateBiquadFilterCommand(
node_id, effect_info, buffer_offset, channel, needs_init,
render_context.behavior->UseBiquadFilterFloatProcessing());
}
} else {
for (s8 channel = 0; channel < parameter.channel_count; channel++) {
command_buffer.GenerateCopyMixBufferCommand(node_id, effect_info, buffer_offset,
channel);
}
}
}
void CommandGenerator::GenerateLightLimiterEffectCommand(const s16 buffer_offset,
EffectInfoBase& effect_info,
const s32 node_id,
const u32 effect_index) {
const auto& state{*reinterpret_cast<LightLimiterInfo::State*>(effect_info.GetStateBuffer())};
if (render_context.behavior->IsEffectInfoVersion2Supported()) {
const auto& parameter{
*reinterpret_cast<LightLimiterInfo::ParameterVersion2*>(effect_info.GetParameter())};
const auto& result_state{*reinterpret_cast<LightLimiterInfo::StatisticsInternal*>(
&effect_context.GetDspSharedResultState(effect_index))};
command_buffer.GenerateLightLimiterCommand(node_id, buffer_offset, parameter, result_state,
state, effect_info.IsEnabled(),
effect_info.GetWorkbuffer(-1));
} else {
const auto& parameter{
*reinterpret_cast<LightLimiterInfo::ParameterVersion1*>(effect_info.GetParameter())};
command_buffer.GenerateLightLimiterCommand(node_id, buffer_offset, parameter, state,
effect_info.IsEnabled(),
effect_info.GetWorkbuffer(-1));
}
}
void CommandGenerator::GenerateCaptureCommand(const s16 buffer_offset, EffectInfoBase& effect_info,
const s32 node_id) {
if (effect_info.IsEnabled()) {
effect_info.GetWorkbuffer(0);
}
if (effect_info.GetSendBuffer()) {
const auto& parameter{
*reinterpret_cast<AuxInfo::ParameterVersion1*>(effect_info.GetParameter())};
auto channel_index{parameter.mix_buffer_count - 1};
u32 write_offset{0};
for (u32 i = 0; i < parameter.mix_buffer_count; i++, channel_index--) {
auto new_update_count{command_header.sample_count + write_offset};
const auto update_count{channel_index > 0 ? 0 : new_update_count};
command_buffer.GenerateCaptureCommand(node_id, effect_info, parameter.inputs[i],
parameter.outputs[i], buffer_offset, update_count,
parameter.count_max, write_offset);
write_offset = new_update_count;
}
}
}
void CommandGenerator::GenerateCompressorCommand(const s16 buffer_offset,
EffectInfoBase& effect_info, const s32 node_id) {
command_buffer.GenerateCompressorCommand(buffer_offset, effect_info, node_id);
}
void CommandGenerator::GenerateEffectCommand(MixInfo& mix_info) {
const auto effect_count{effect_context.GetCount()};
for (u32 i = 0; i < effect_count; i++) {
const auto effect_index{mix_info.effect_order_buffer[i]};
if (effect_index == -1) {
break;
}
auto& effect_info = effect_context.GetInfo(effect_index);
if (effect_info.ShouldSkip()) {
continue;
}
const auto entry_type{mix_info.mix_id == FinalMixId ? PerformanceEntryType::FinalMix
: PerformanceEntryType::SubMix};
switch (effect_info.GetType()) {
case EffectInfoBase::Type::Mix: {
DetailAspect mix_detail_aspect(*this, entry_type, mix_info.node_id,
PerformanceDetailType::Unk5);
GenerateBufferMixerCommand(mix_info.buffer_offset, effect_info, mix_info.node_id);
if (mix_detail_aspect.initialized) {
command_buffer.GeneratePerformanceCommand(
mix_detail_aspect.node_id, PerformanceState::Stop,
mix_detail_aspect.performance_entry_address);
}
} break;
case EffectInfoBase::Type::Aux: {
DetailAspect aux_detail_aspect(*this, entry_type, mix_info.node_id,
PerformanceDetailType::Unk7);
GenerateAuxCommand(mix_info.buffer_offset, effect_info, mix_info.node_id);
if (aux_detail_aspect.initialized) {
command_buffer.GeneratePerformanceCommand(
aux_detail_aspect.node_id, PerformanceState::Stop,
aux_detail_aspect.performance_entry_address);
}
} break;
case EffectInfoBase::Type::Delay: {
DetailAspect delay_detail_aspect(*this, entry_type, mix_info.node_id,
PerformanceDetailType::Unk6);
GenerateDelayCommand(mix_info.buffer_offset, effect_info, mix_info.node_id);
if (delay_detail_aspect.initialized) {
command_buffer.GeneratePerformanceCommand(
delay_detail_aspect.node_id, PerformanceState::Stop,
delay_detail_aspect.performance_entry_address);
}
} break;
case EffectInfoBase::Type::Reverb: {
DetailAspect reverb_detail_aspect(*this, entry_type, mix_info.node_id,
PerformanceDetailType::Unk8);
GenerateReverbCommand(mix_info.buffer_offset, effect_info, mix_info.node_id,
render_context.behavior->IsLongSizePreDelaySupported());
if (reverb_detail_aspect.initialized) {
command_buffer.GeneratePerformanceCommand(
reverb_detail_aspect.node_id, PerformanceState::Stop,
reverb_detail_aspect.performance_entry_address);
}
} break;
case EffectInfoBase::Type::I3dl2Reverb: {
DetailAspect i3dl2_detail_aspect(*this, entry_type, mix_info.node_id,
PerformanceDetailType::Unk9);
GenerateI3dl2ReverbEffectCommand(mix_info.buffer_offset, effect_info, mix_info.node_id);
if (i3dl2_detail_aspect.initialized) {
command_buffer.GeneratePerformanceCommand(
i3dl2_detail_aspect.node_id, PerformanceState::Stop,
i3dl2_detail_aspect.performance_entry_address);
}
} break;
case EffectInfoBase::Type::BiquadFilter: {
DetailAspect biquad_detail_aspect(*this, entry_type, mix_info.node_id,
PerformanceDetailType::Unk4);
GenerateBiquadFilterEffectCommand(mix_info.buffer_offset, effect_info,
mix_info.node_id);
if (biquad_detail_aspect.initialized) {
command_buffer.GeneratePerformanceCommand(
biquad_detail_aspect.node_id, PerformanceState::Stop,
biquad_detail_aspect.performance_entry_address);
}
} break;
case EffectInfoBase::Type::LightLimiter: {
DetailAspect light_limiter_detail_aspect(*this, entry_type, mix_info.node_id,
PerformanceDetailType::Unk11);
GenerateLightLimiterEffectCommand(mix_info.buffer_offset, effect_info, mix_info.node_id,
effect_index);
if (light_limiter_detail_aspect.initialized) {
command_buffer.GeneratePerformanceCommand(
light_limiter_detail_aspect.node_id, PerformanceState::Stop,
light_limiter_detail_aspect.performance_entry_address);
}
} break;
case EffectInfoBase::Type::Capture: {
DetailAspect capture_detail_aspect(*this, entry_type, mix_info.node_id,
PerformanceDetailType::Unk12);
GenerateCaptureCommand(mix_info.buffer_offset, effect_info, mix_info.node_id);
if (capture_detail_aspect.initialized) {
command_buffer.GeneratePerformanceCommand(
capture_detail_aspect.node_id, PerformanceState::Stop,
capture_detail_aspect.performance_entry_address);
}
} break;
case EffectInfoBase::Type::Compressor: {
DetailAspect capture_detail_aspect(*this, entry_type, mix_info.node_id,
PerformanceDetailType::Unk13);
GenerateCompressorCommand(mix_info.buffer_offset, effect_info, mix_info.node_id);
if (capture_detail_aspect.initialized) {
command_buffer.GeneratePerformanceCommand(
capture_detail_aspect.node_id, PerformanceState::Stop,
capture_detail_aspect.performance_entry_address);
}
} break;
default:
LOG_ERROR(Service_Audio, "Invalid effect type {}",
static_cast<u32>(effect_info.GetType()));
break;
}
effect_info.UpdateForCommandGeneration();
}
}
void CommandGenerator::GenerateMixCommands(MixInfo& mix_info) {
u8 precision{15};
if (render_context.behavior->IsVolumeMixParameterPrecisionQ23Supported()) {
precision = 23;
}
if (!mix_info.HasAnyConnection()) {
return;
}
if (mix_info.dst_mix_id == UnusedMixId) {
if (mix_info.dst_splitter_id != UnusedSplitterId) {
s16 dest_id{0};
auto destination{
splitter_context.GetDestinationData(mix_info.dst_splitter_id, dest_id)};
while (destination != nullptr) {
if (destination->IsConfigured()) {
auto splitter_mix_id{destination->GetMixId()};
if (splitter_mix_id < mix_context.GetCount()) {
auto splitter_mix_info{mix_context.GetInfo(splitter_mix_id)};
const s16 input_index{static_cast<s16>(mix_info.buffer_offset +
(dest_id % mix_info.buffer_count))};
for (s16 i = 0; i < splitter_mix_info->buffer_count; i++) {
auto volume{mix_info.volume * destination->GetMixVolume(i)};
if (volume != 0.0f) {
command_buffer.GenerateMixCommand(
mix_info.node_id, input_index,
splitter_mix_info->buffer_offset + i, mix_info.buffer_offset,
volume, precision);
}
}
}
}
dest_id++;
destination =
splitter_context.GetDestinationData(mix_info.dst_splitter_id, dest_id);
}
}
} else {
auto dest_mix_info{mix_context.GetInfo(mix_info.dst_mix_id)};
for (s16 i = 0; i < mix_info.buffer_count; i++) {
for (s16 j = 0; j < dest_mix_info->buffer_count; j++) {
auto volume{mix_info.volume * mix_info.mix_volumes[i][j]};
if (volume != 0.0f) {
command_buffer.GenerateMixCommand(mix_info.node_id, mix_info.buffer_offset + i,
dest_mix_info->buffer_offset + j,
mix_info.buffer_offset, volume, precision);
}
}
}
}
}
void CommandGenerator::GenerateSubMixCommand(MixInfo& mix_info) {
command_buffer.GenerateDepopForMixBuffersCommand(mix_info.node_id, mix_info,
render_context.depop_buffer);
GenerateEffectCommand(mix_info);
DetailAspect mix_detail_aspect(*this, PerformanceEntryType::SubMix, mix_info.node_id,
PerformanceDetailType::Unk5);
GenerateMixCommands(mix_info);
if (mix_detail_aspect.initialized) {
command_buffer.GeneratePerformanceCommand(mix_detail_aspect.node_id, PerformanceState::Stop,
mix_detail_aspect.performance_entry_address);
}
}
void CommandGenerator::GenerateSubMixCommands() {
const auto submix_count{mix_context.GetCount()};
for (s32 i = 0; i < submix_count; i++) {
auto sorted_info{mix_context.GetSortedInfo(i)};
if (!sorted_info->in_use || sorted_info->mix_id == FinalMixId) {
continue;
}
EntryAspect submix_entry_aspect(*this, PerformanceEntryType::SubMix, sorted_info->node_id);
GenerateSubMixCommand(*sorted_info);
if (submix_entry_aspect.initialized) {
command_buffer.GeneratePerformanceCommand(
submix_entry_aspect.node_id, PerformanceState::Stop,
submix_entry_aspect.performance_entry_address);
}
}
}
void CommandGenerator::GenerateFinalMixCommand() {
auto& final_mix_info{*mix_context.GetFinalMixInfo()};
command_buffer.GenerateDepopForMixBuffersCommand(final_mix_info.node_id, final_mix_info,
render_context.depop_buffer);
GenerateEffectCommand(final_mix_info);
u8 precision{15};
if (render_context.behavior->IsVolumeMixParameterPrecisionQ23Supported()) {
precision = 23;
}
for (s16 i = 0; i < final_mix_info.buffer_count; i++) {
DetailAspect volume_aspect(*this, PerformanceEntryType::FinalMix, final_mix_info.node_id,
PerformanceDetailType::Unk3);
command_buffer.GenerateVolumeCommand(final_mix_info.node_id, final_mix_info.buffer_offset,
i, final_mix_info.volume, precision);
if (volume_aspect.initialized) {
command_buffer.GeneratePerformanceCommand(volume_aspect.node_id, PerformanceState::Stop,
volume_aspect.performance_entry_address);
}
}
}
void CommandGenerator::GenerateFinalMixCommands() {
auto final_mix_info{mix_context.GetFinalMixInfo()};
EntryAspect final_mix_entry(*this, PerformanceEntryType::FinalMix, final_mix_info->node_id);
GenerateFinalMixCommand();
if (final_mix_entry.initialized) {
command_buffer.GeneratePerformanceCommand(final_mix_entry.node_id, PerformanceState::Stop,
final_mix_entry.performance_entry_address);
}
}
void CommandGenerator::GenerateSinkCommands() {
const auto sink_count{sink_context.GetCount()};
for (u32 i = 0; i < sink_count; i++) {
auto sink_info{sink_context.GetInfo(i)};
if (sink_info->IsUsed() && sink_info->GetType() == SinkInfoBase::Type::DeviceSink) {
auto state{reinterpret_cast<DeviceSinkInfo::DeviceState*>(sink_info->GetState())};
if (command_header.sample_rate != TargetSampleRate &&
state->upsampler_info == nullptr) {
auto device_state{sink_info->GetDeviceState()};
device_state->upsampler_info = render_context.upsampler_manager->Allocate();
}
EntryAspect device_sink_entry(*this, PerformanceEntryType::Sink,
sink_info->GetNodeId());
auto final_mix{mix_context.GetFinalMixInfo()};
GenerateSinkCommand(final_mix->buffer_offset, *sink_info);
if (device_sink_entry.initialized) {
command_buffer.GeneratePerformanceCommand(
device_sink_entry.node_id, PerformanceState::Stop,
device_sink_entry.performance_entry_address);
}
}
}
for (u32 i = 0; i < sink_count; i++) {
auto sink_info{sink_context.GetInfo(i)};
if (sink_info->IsUsed() && sink_info->GetType() == SinkInfoBase::Type::CircularBufferSink) {
EntryAspect circular_buffer_entry(*this, PerformanceEntryType::Sink,
sink_info->GetNodeId());
auto final_mix{mix_context.GetFinalMixInfo()};
GenerateSinkCommand(final_mix->buffer_offset, *sink_info);
if (circular_buffer_entry.initialized) {
command_buffer.GeneratePerformanceCommand(
circular_buffer_entry.node_id, PerformanceState::Stop,
circular_buffer_entry.performance_entry_address);
}
}
}
}
void CommandGenerator::GenerateSinkCommand(const s16 buffer_offset, SinkInfoBase& sink_info) {
if (sink_info.ShouldSkip()) {
return;
}
switch (sink_info.GetType()) {
case SinkInfoBase::Type::DeviceSink:
GenerateDeviceSinkCommand(buffer_offset, sink_info);
break;
case SinkInfoBase::Type::CircularBufferSink:
command_buffer.GenerateCircularBufferSinkCommand(sink_info.GetNodeId(), sink_info,
buffer_offset);
break;
default:
LOG_ERROR(Service_Audio, "Invalid sink type {}", static_cast<u32>(sink_info.GetType()));
break;
}
sink_info.UpdateForCommandGeneration();
}
void CommandGenerator::GenerateDeviceSinkCommand(const s16 buffer_offset, SinkInfoBase& sink_info) {
auto& parameter{
*reinterpret_cast<DeviceSinkInfo::DeviceInParameter*>(sink_info.GetParameter())};
auto state{*reinterpret_cast<DeviceSinkInfo::DeviceState*>(sink_info.GetState())};
if (render_context.channels == 2 && parameter.downmix_enabled) {
command_buffer.GenerateDownMix6chTo2chCommand(InvalidNodeId, parameter.inputs,
buffer_offset, parameter.downmix_coeff);
}
if (state.upsampler_info != nullptr) {
command_buffer.GenerateUpsampleCommand(
InvalidNodeId, buffer_offset, *state.upsampler_info, parameter.input_count,
parameter.inputs, command_header.buffer_count, command_header.sample_count,
command_header.sample_rate);
}
command_buffer.GenerateDeviceSinkCommand(InvalidNodeId, buffer_offset, sink_info,
render_context.session_id,
command_header.samples_buffer);
}
void CommandGenerator::GeneratePerformanceCommand(
s32 node_id, PerformanceState state, const PerformanceEntryAddresses& entry_addresses) {
command_buffer.GeneratePerformanceCommand(node_id, state, entry_addresses);
}
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,349 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <span>
#include "audio_core/renderer/command/commands.h"
#include "audio_core/renderer/performance/performance_manager.h"
#include "common/common_types.h"
namespace AudioCore {
struct AudioRendererSystemContext;
namespace Renderer {
class CommandBuffer;
struct CommandListHeader;
class VoiceContext;
class MixContext;
class EffectContext;
class SplitterContext;
class SinkContext;
class BehaviorInfo;
class VoiceInfo;
struct VoiceState;
class MixInfo;
class SinkInfoBase;
/**
* Generates all commands to build up a command list, which are sent to the AudioRender for
* processing.
*/
class CommandGenerator {
public:
explicit CommandGenerator(CommandBuffer& command_buffer,
const CommandListHeader& command_list_header,
const AudioRendererSystemContext& render_context,
VoiceContext& voice_context, MixContext& mix_context,
EffectContext& effect_context, SinkContext& sink_context,
SplitterContext& splitter_context,
PerformanceManager* performance_manager);
/**
* Calculate the buffer size needed for commands.
*
* @param behavior - Used to check what features are enabled.
* @param params - Input rendering parameters for numbers of voices/mixes/sinks etc.
*/
static u64 CalculateCommandBufferSize(const BehaviorInfo& behavior,
const AudioRendererParameterInternal& params) {
u64 size{0};
// Effects
size += params.effects * sizeof(EffectInfoBase);
// Voices
u64 voice_size{0};
if (behavior.IsWaveBufferVer2Supported()) {
voice_size = std::max(std::max(sizeof(AdpcmDataSourceVersion2Command),
sizeof(PcmInt16DataSourceVersion2Command)),
sizeof(PcmFloatDataSourceVersion2Command));
} else {
voice_size = std::max(std::max(sizeof(AdpcmDataSourceVersion1Command),
sizeof(PcmInt16DataSourceVersion1Command)),
sizeof(PcmFloatDataSourceVersion1Command));
}
voice_size += sizeof(BiquadFilterCommand) * MaxBiquadFilters;
voice_size += sizeof(VolumeRampCommand);
voice_size += sizeof(MixRampGroupedCommand);
size += params.voices * (params.splitter_infos * sizeof(DepopPrepareCommand) + voice_size);
// Sub mixes
size += sizeof(DepopForMixBuffersCommand) +
(sizeof(MixCommand) * MaxMixBuffers) * MaxMixBuffers;
// Final mix
size += sizeof(DepopForMixBuffersCommand) + sizeof(VolumeCommand) * MaxMixBuffers;
// Splitters
size += params.splitter_destinations * sizeof(MixRampCommand) * MaxMixBuffers;
// Sinks
size +=
params.sinks * std::max(sizeof(DeviceSinkCommand), sizeof(CircularBufferSinkCommand));
// Performance
size += (params.effects + params.voices + params.sinks + params.sub_mixes + 1 +
PerformanceManager::MaxDetailEntries) *
sizeof(PerformanceCommand);
return size;
}
/**
* Get the current command buffer used to generate commands.
*
* @return The command buffer.
*/
CommandBuffer& GetCommandBuffer() {
return command_buffer;
}
/**
* Get the current performance manager,
*
* @return The performance manager. May be nullptr.
*/
PerformanceManager* GetPerformanceManager() {
return performance_manager;
}
/**
* Generate a data source command.
* These are the basis for all audio output.
*
* @param voice_info - Generate the command from this voice.
* @param voice_state - State used by the AudioRenderer across calls.
* @param channel - Channel index to generate the command into.
*/
void GenerateDataSourceCommand(VoiceInfo& voice_info, const VoiceState& voice_state,
s8 channel);
/**
* Generate voice mixing commands.
* These are used to mix buffers together, to mix one input to many outputs,
* and also used as copy commands to move data around and prevent it being accidentally
* overwritten, e.g by another data source command into the same channel.
*
* @param mix_volumes - Current volumes of the mix.
* @param prev_mix_volumes - Previous volumes of the mix.
* @param voice_state - State used by the AudioRenderer across calls.
* @param output_index - Output mix buffer index.
* @param buffer_count - Number of active mix buffers.
* @param input_index - Input mix buffer index.
* @param node_id - Node id of the voice this command is generated for.
*/
void GenerateVoiceMixCommand(std::span<const f32> mix_volumes,
std::span<const f32> prev_mix_volumes,
const VoiceState& voice_state, s16 output_index, s16 buffer_count,
s16 input_index, s32 node_id);
/**
* Generate a biquad filter command for a voice.
*
* @param voice_info - Voice info this command is generated from.
* @param voice_state - State used by the AudioRenderer across calls.
* @param buffer_count - Number of active mix buffers.
* @param channel - Channel index of this command.
* @param node_id - Node id of the voice this command is generated for.
*/
void GenerateBiquadFilterCommandForVoice(VoiceInfo& voice_info, const VoiceState& voice_state,
s16 buffer_count, s8 channel, s32 node_id);
/**
* Generate commands for a voice.
* Includes a data source, biquad filter, volume and mixing.
*
* @param voice_info - Voice info these commands are generated from.
*/
void GenerateVoiceCommand(VoiceInfo& voice_info);
/**
* Generate commands for all voices.
*/
void GenerateVoiceCommands();
/**
* Generate a mixing command.
*
* @param buffer_offset - Base mix buffer offset to use.
* @param effect_info_base - BufferMixer effect info.
* @param node_id - Node id of the mix this command is generated for.
*/
void GenerateBufferMixerCommand(s16 buffer_offset, EffectInfoBase& effect_info_base,
s32 node_id);
/**
* Generate a delay effect command.
*
* @param buffer_offset - Base mix buffer offset to use.
* @param effect_info_base - Delay effect info.
* @param node_id - Node id of the mix this command is generated for.
*/
void GenerateDelayCommand(s16 buffer_offset, EffectInfoBase& effect_info_base, s32 node_id);
/**
* Generate a reverb effect command.
*
* @param buffer_offset - Base mix buffer offset to use.
* @param effect_info_base - Reverb effect info.
* @param node_id - Node id of the mix this command is generated for.
* @param long_size_pre_delay_supported - Use a longer pre-delay time before reverb starts.
*/
void GenerateReverbCommand(s16 buffer_offset, EffectInfoBase& effect_info_base, s32 node_id,
bool long_size_pre_delay_supported);
/**
* Generate an I3DL2 reverb effect command.
*
* @param buffer_offset - Base mix buffer offset to use.
* @param effect_info - I3DL2Reverb effect info.
* @param node_id - Node id of the mix this command is generated for.
*/
void GenerateI3dl2ReverbEffectCommand(s16 buffer_offset, EffectInfoBase& effect_info,
s32 node_id);
/**
* Generate an aux effect command.
*
* @param buffer_offset - Base mix buffer offset to use.
* @param effect_info - Aux effect info.
* @param node_id - Node id of the mix this command is generated for.
*/
void GenerateAuxCommand(s16 buffer_offset, EffectInfoBase& effect_info, s32 node_id);
/**
* Generate a biquad filter effect command.
*
* @param buffer_offset - Base mix buffer offset to use.
* @param effect_info - Aux effect info.
* @param node_id - Node id of the mix this command is generated for.
*/
void GenerateBiquadFilterEffectCommand(s16 buffer_offset, EffectInfoBase& effect_info,
s32 node_id);
/**
* Generate a light limiter effect command.
*
* @param buffer_offset - Base mix buffer offset to use.
* @param effect_info - Limiter effect info.
* @param node_id - Node id of the mix this command is generated for.
* @param effect_index - Index for the statistics state.
*/
void GenerateLightLimiterEffectCommand(s16 buffer_offset, EffectInfoBase& effect_info,
s32 node_id, u32 effect_index);
/**
* Generate a capture effect command.
* Writes a mix buffer back to game memory.
*
* @param buffer_offset - Base mix buffer offset to use.
* @param effect_info - Capture effect info.
* @param node_id - Node id of the mix this command is generated for.
*/
void GenerateCaptureCommand(s16 buffer_offset, EffectInfoBase& effect_info, s32 node_id);
/**
* Generate a compressor effect command.
*
* @param buffer_offset - Base mix buffer offset to use.
* @param effect_info - Compressor effect info.
* @param node_id - Node id of the mix this command is generated for.
*/
void GenerateCompressorCommand(s16 buffer_offset, EffectInfoBase& effect_info, s32 node_id);
/**
* Generate all effect commands for a mix.
*
* @param mix_info - Mix to generate effects from.
*/
void GenerateEffectCommand(MixInfo& mix_info);
/**
* Generate all mix commands.
*
* @param mix_info - Mix to generate effects from.
*/
void GenerateMixCommands(MixInfo& mix_info);
/**
* Generate a submix command.
* Generates all effects and all mixing commands.
*
* @param mix_info - Mix to generate effects from.
*/
void GenerateSubMixCommand(MixInfo& mix_info);
/**
* Generate all submix command.
*/
void GenerateSubMixCommands();
/**
* Generate the final mix.
*/
void GenerateFinalMixCommand();
/**
* Generate the final mix commands.
*/
void GenerateFinalMixCommands();
/**
* Generate all sink commands.
*/
void GenerateSinkCommands();
/**
* Generate a sink command.
* Sends samples out to the backend, or a game-supplied circular buffer.
*
* @param buffer_offset - Base mix buffer offset to use.
* @param sink_info - Sink info to generate the commands from.
*/
void GenerateSinkCommand(s16 buffer_offset, SinkInfoBase& sink_info);
/**
* Generate a device sink command.
* Sends samples out to the backend.
*
* @param buffer_offset - Base mix buffer offset to use.
* @param sink_info - Sink info to generate the commands from.
*/
void GenerateDeviceSinkCommand(s16 buffer_offset, SinkInfoBase& sink_info);
/**
* Generate a performance command.
* Used to report performance metrics of the AudioRenderer back to the game.
*
* @param node_id - Node ID of the mix this command is generated for
* @param state - Output state of the generated performance command
* @param entry_addresses - Addresses to be written
*/
void GeneratePerformanceCommand(s32 node_id, PerformanceState state,
const PerformanceEntryAddresses& entry_addresses);
private:
/// Commands will be written by this buffer
CommandBuffer& command_buffer;
/// Header information for the commands generated
const CommandListHeader& command_header;
/// Various things to control generation
const AudioRendererSystemContext& render_context;
/// Used for generating voices
VoiceContext& voice_context;
/// Used for generating mixes
MixContext& mix_context;
/// Used for generating effects
EffectContext& effect_context;
/// Used for generating sinks
SinkContext& sink_context;
/// Used for generating submixes
SplitterContext& splitter_context;
/// Used for generating performance
PerformanceManager* performance_manager;
};
} // namespace Renderer
} // namespace AudioCore

View File

@@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <span>
#include "audio_core/common/common.h"
#include "common/common_types.h"
namespace AudioCore::Renderer {
struct CommandListHeader {
u64 buffer_size;
u32 command_count;
std::span<s32> samples_buffer;
s16 buffer_count;
u32 sample_count;
u32 sample_rate;
};
} // namespace AudioCore::Renderer

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,254 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "audio_core/renderer/command/commands.h"
#include "common/common_types.h"
namespace AudioCore::Renderer {
/**
* Estimate the processing time required for all commands.
*/
class ICommandProcessingTimeEstimator {
public:
virtual ~ICommandProcessingTimeEstimator() = default;
virtual u32 Estimate(const PcmInt16DataSourceVersion1Command& command) const = 0;
virtual u32 Estimate(const PcmInt16DataSourceVersion2Command& command) const = 0;
virtual u32 Estimate(const PcmFloatDataSourceVersion1Command& command) const = 0;
virtual u32 Estimate(const PcmFloatDataSourceVersion2Command& command) const = 0;
virtual u32 Estimate(const AdpcmDataSourceVersion1Command& command) const = 0;
virtual u32 Estimate(const AdpcmDataSourceVersion2Command& command) const = 0;
virtual u32 Estimate(const VolumeCommand& command) const = 0;
virtual u32 Estimate(const VolumeRampCommand& command) const = 0;
virtual u32 Estimate(const BiquadFilterCommand& command) const = 0;
virtual u32 Estimate(const MixCommand& command) const = 0;
virtual u32 Estimate(const MixRampCommand& command) const = 0;
virtual u32 Estimate(const MixRampGroupedCommand& command) const = 0;
virtual u32 Estimate(const DepopPrepareCommand& command) const = 0;
virtual u32 Estimate(const DepopForMixBuffersCommand& command) const = 0;
virtual u32 Estimate(const DelayCommand& command) const = 0;
virtual u32 Estimate(const UpsampleCommand& command) const = 0;
virtual u32 Estimate(const DownMix6chTo2chCommand& command) const = 0;
virtual u32 Estimate(const AuxCommand& command) const = 0;
virtual u32 Estimate(const DeviceSinkCommand& command) const = 0;
virtual u32 Estimate(const CircularBufferSinkCommand& command) const = 0;
virtual u32 Estimate(const ReverbCommand& command) const = 0;
virtual u32 Estimate(const I3dl2ReverbCommand& command) const = 0;
virtual u32 Estimate(const PerformanceCommand& command) const = 0;
virtual u32 Estimate(const ClearMixBufferCommand& command) const = 0;
virtual u32 Estimate(const CopyMixBufferCommand& command) const = 0;
virtual u32 Estimate(const LightLimiterVersion1Command& command) const = 0;
virtual u32 Estimate(const LightLimiterVersion2Command& command) const = 0;
virtual u32 Estimate(const MultiTapBiquadFilterCommand& command) const = 0;
virtual u32 Estimate(const CaptureCommand& command) const = 0;
virtual u32 Estimate(const CompressorCommand& command) const = 0;
};
class CommandProcessingTimeEstimatorVersion1 final : public ICommandProcessingTimeEstimator {
public:
CommandProcessingTimeEstimatorVersion1(u32 sample_count_, u32 buffer_count_)
: sample_count{sample_count_}, buffer_count{buffer_count_} {}
u32 Estimate(const PcmInt16DataSourceVersion1Command& command) const override;
u32 Estimate(const PcmInt16DataSourceVersion2Command& command) const override;
u32 Estimate(const PcmFloatDataSourceVersion1Command& command) const override;
u32 Estimate(const PcmFloatDataSourceVersion2Command& command) const override;
u32 Estimate(const AdpcmDataSourceVersion1Command& command) const override;
u32 Estimate(const AdpcmDataSourceVersion2Command& command) const override;
u32 Estimate(const VolumeCommand& command) const override;
u32 Estimate(const VolumeRampCommand& command) const override;
u32 Estimate(const BiquadFilterCommand& command) const override;
u32 Estimate(const MixCommand& command) const override;
u32 Estimate(const MixRampCommand& command) const override;
u32 Estimate(const MixRampGroupedCommand& command) const override;
u32 Estimate(const DepopPrepareCommand& command) const override;
u32 Estimate(const DepopForMixBuffersCommand& command) const override;
u32 Estimate(const DelayCommand& command) const override;
u32 Estimate(const UpsampleCommand& command) const override;
u32 Estimate(const DownMix6chTo2chCommand& command) const override;
u32 Estimate(const AuxCommand& command) const override;
u32 Estimate(const DeviceSinkCommand& command) const override;
u32 Estimate(const CircularBufferSinkCommand& command) const override;
u32 Estimate(const ReverbCommand& command) const override;
u32 Estimate(const I3dl2ReverbCommand& command) const override;
u32 Estimate(const PerformanceCommand& command) const override;
u32 Estimate(const ClearMixBufferCommand& command) const override;
u32 Estimate(const CopyMixBufferCommand& command) const override;
u32 Estimate(const LightLimiterVersion1Command& command) const override;
u32 Estimate(const LightLimiterVersion2Command& command) const override;
u32 Estimate(const MultiTapBiquadFilterCommand& command) const override;
u32 Estimate(const CaptureCommand& command) const override;
u32 Estimate(const CompressorCommand& command) const override;
private:
u32 sample_count{};
u32 buffer_count{};
};
class CommandProcessingTimeEstimatorVersion2 final : public ICommandProcessingTimeEstimator {
public:
CommandProcessingTimeEstimatorVersion2(u32 sample_count_, u32 buffer_count_)
: sample_count{sample_count_}, buffer_count{buffer_count_} {}
u32 Estimate(const PcmInt16DataSourceVersion1Command& command) const override;
u32 Estimate(const PcmInt16DataSourceVersion2Command& command) const override;
u32 Estimate(const PcmFloatDataSourceVersion1Command& command) const override;
u32 Estimate(const PcmFloatDataSourceVersion2Command& command) const override;
u32 Estimate(const AdpcmDataSourceVersion1Command& command) const override;
u32 Estimate(const AdpcmDataSourceVersion2Command& command) const override;
u32 Estimate(const VolumeCommand& command) const override;
u32 Estimate(const VolumeRampCommand& command) const override;
u32 Estimate(const BiquadFilterCommand& command) const override;
u32 Estimate(const MixCommand& command) const override;
u32 Estimate(const MixRampCommand& command) const override;
u32 Estimate(const MixRampGroupedCommand& command) const override;
u32 Estimate(const DepopPrepareCommand& command) const override;
u32 Estimate(const DepopForMixBuffersCommand& command) const override;
u32 Estimate(const DelayCommand& command) const override;
u32 Estimate(const UpsampleCommand& command) const override;
u32 Estimate(const DownMix6chTo2chCommand& command) const override;
u32 Estimate(const AuxCommand& command) const override;
u32 Estimate(const DeviceSinkCommand& command) const override;
u32 Estimate(const CircularBufferSinkCommand& command) const override;
u32 Estimate(const ReverbCommand& command) const override;
u32 Estimate(const I3dl2ReverbCommand& command) const override;
u32 Estimate(const PerformanceCommand& command) const override;
u32 Estimate(const ClearMixBufferCommand& command) const override;
u32 Estimate(const CopyMixBufferCommand& command) const override;
u32 Estimate(const LightLimiterVersion1Command& command) const override;
u32 Estimate(const LightLimiterVersion2Command& command) const override;
u32 Estimate(const MultiTapBiquadFilterCommand& command) const override;
u32 Estimate(const CaptureCommand& command) const override;
u32 Estimate(const CompressorCommand& command) const override;
private:
u32 sample_count{};
u32 buffer_count{};
};
class CommandProcessingTimeEstimatorVersion3 final : public ICommandProcessingTimeEstimator {
public:
CommandProcessingTimeEstimatorVersion3(u32 sample_count_, u32 buffer_count_)
: sample_count{sample_count_}, buffer_count{buffer_count_} {}
u32 Estimate(const PcmInt16DataSourceVersion1Command& command) const override;
u32 Estimate(const PcmInt16DataSourceVersion2Command& command) const override;
u32 Estimate(const PcmFloatDataSourceVersion1Command& command) const override;
u32 Estimate(const PcmFloatDataSourceVersion2Command& command) const override;
u32 Estimate(const AdpcmDataSourceVersion1Command& command) const override;
u32 Estimate(const AdpcmDataSourceVersion2Command& command) const override;
u32 Estimate(const VolumeCommand& command) const override;
u32 Estimate(const VolumeRampCommand& command) const override;
u32 Estimate(const BiquadFilterCommand& command) const override;
u32 Estimate(const MixCommand& command) const override;
u32 Estimate(const MixRampCommand& command) const override;
u32 Estimate(const MixRampGroupedCommand& command) const override;
u32 Estimate(const DepopPrepareCommand& command) const override;
u32 Estimate(const DepopForMixBuffersCommand& command) const override;
u32 Estimate(const DelayCommand& command) const override;
u32 Estimate(const UpsampleCommand& command) const override;
u32 Estimate(const DownMix6chTo2chCommand& command) const override;
u32 Estimate(const AuxCommand& command) const override;
u32 Estimate(const DeviceSinkCommand& command) const override;
u32 Estimate(const CircularBufferSinkCommand& command) const override;
u32 Estimate(const ReverbCommand& command) const override;
u32 Estimate(const I3dl2ReverbCommand& command) const override;
u32 Estimate(const PerformanceCommand& command) const override;
u32 Estimate(const ClearMixBufferCommand& command) const override;
u32 Estimate(const CopyMixBufferCommand& command) const override;
u32 Estimate(const LightLimiterVersion1Command& command) const override;
u32 Estimate(const LightLimiterVersion2Command& command) const override;
u32 Estimate(const MultiTapBiquadFilterCommand& command) const override;
u32 Estimate(const CaptureCommand& command) const override;
u32 Estimate(const CompressorCommand& command) const override;
private:
u32 sample_count{};
u32 buffer_count{};
};
class CommandProcessingTimeEstimatorVersion4 final : public ICommandProcessingTimeEstimator {
public:
CommandProcessingTimeEstimatorVersion4(u32 sample_count_, u32 buffer_count_)
: sample_count{sample_count_}, buffer_count{buffer_count_} {}
u32 Estimate(const PcmInt16DataSourceVersion1Command& command) const override;
u32 Estimate(const PcmInt16DataSourceVersion2Command& command) const override;
u32 Estimate(const PcmFloatDataSourceVersion1Command& command) const override;
u32 Estimate(const PcmFloatDataSourceVersion2Command& command) const override;
u32 Estimate(const AdpcmDataSourceVersion1Command& command) const override;
u32 Estimate(const AdpcmDataSourceVersion2Command& command) const override;
u32 Estimate(const VolumeCommand& command) const override;
u32 Estimate(const VolumeRampCommand& command) const override;
u32 Estimate(const BiquadFilterCommand& command) const override;
u32 Estimate(const MixCommand& command) const override;
u32 Estimate(const MixRampCommand& command) const override;
u32 Estimate(const MixRampGroupedCommand& command) const override;
u32 Estimate(const DepopPrepareCommand& command) const override;
u32 Estimate(const DepopForMixBuffersCommand& command) const override;
u32 Estimate(const DelayCommand& command) const override;
u32 Estimate(const UpsampleCommand& command) const override;
u32 Estimate(const DownMix6chTo2chCommand& command) const override;
u32 Estimate(const AuxCommand& command) const override;
u32 Estimate(const DeviceSinkCommand& command) const override;
u32 Estimate(const CircularBufferSinkCommand& command) const override;
u32 Estimate(const ReverbCommand& command) const override;
u32 Estimate(const I3dl2ReverbCommand& command) const override;
u32 Estimate(const PerformanceCommand& command) const override;
u32 Estimate(const ClearMixBufferCommand& command) const override;
u32 Estimate(const CopyMixBufferCommand& command) const override;
u32 Estimate(const LightLimiterVersion1Command& command) const override;
u32 Estimate(const LightLimiterVersion2Command& command) const override;
u32 Estimate(const MultiTapBiquadFilterCommand& command) const override;
u32 Estimate(const CaptureCommand& command) const override;
u32 Estimate(const CompressorCommand& command) const override;
private:
u32 sample_count{};
u32 buffer_count{};
};
class CommandProcessingTimeEstimatorVersion5 final : public ICommandProcessingTimeEstimator {
public:
CommandProcessingTimeEstimatorVersion5(u32 sample_count_, u32 buffer_count_)
: sample_count{sample_count_}, buffer_count{buffer_count_} {}
u32 Estimate(const PcmInt16DataSourceVersion1Command& command) const override;
u32 Estimate(const PcmInt16DataSourceVersion2Command& command) const override;
u32 Estimate(const PcmFloatDataSourceVersion1Command& command) const override;
u32 Estimate(const PcmFloatDataSourceVersion2Command& command) const override;
u32 Estimate(const AdpcmDataSourceVersion1Command& command) const override;
u32 Estimate(const AdpcmDataSourceVersion2Command& command) const override;
u32 Estimate(const VolumeCommand& command) const override;
u32 Estimate(const VolumeRampCommand& command) const override;
u32 Estimate(const BiquadFilterCommand& command) const override;
u32 Estimate(const MixCommand& command) const override;
u32 Estimate(const MixRampCommand& command) const override;
u32 Estimate(const MixRampGroupedCommand& command) const override;
u32 Estimate(const DepopPrepareCommand& command) const override;
u32 Estimate(const DepopForMixBuffersCommand& command) const override;
u32 Estimate(const DelayCommand& command) const override;
u32 Estimate(const UpsampleCommand& command) const override;
u32 Estimate(const DownMix6chTo2chCommand& command) const override;
u32 Estimate(const AuxCommand& command) const override;
u32 Estimate(const DeviceSinkCommand& command) const override;
u32 Estimate(const CircularBufferSinkCommand& command) const override;
u32 Estimate(const ReverbCommand& command) const override;
u32 Estimate(const I3dl2ReverbCommand& command) const override;
u32 Estimate(const PerformanceCommand& command) const override;
u32 Estimate(const ClearMixBufferCommand& command) const override;
u32 Estimate(const CopyMixBufferCommand& command) const override;
u32 Estimate(const LightLimiterVersion1Command& command) const override;
u32 Estimate(const LightLimiterVersion2Command& command) const override;
u32 Estimate(const MultiTapBiquadFilterCommand& command) const override;
u32 Estimate(const CaptureCommand& command) const override;
u32 Estimate(const CompressorCommand& command) const override;
private:
u32 sample_count{};
u32 buffer_count{};
};
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,32 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "audio_core/renderer/command/data_source/adpcm.h"
#include "audio_core/renderer/command/data_source/pcm_float.h"
#include "audio_core/renderer/command/data_source/pcm_int16.h"
#include "audio_core/renderer/command/effect/aux_.h"
#include "audio_core/renderer/command/effect/biquad_filter.h"
#include "audio_core/renderer/command/effect/capture.h"
#include "audio_core/renderer/command/effect/compressor.h"
#include "audio_core/renderer/command/effect/delay.h"
#include "audio_core/renderer/command/effect/i3dl2_reverb.h"
#include "audio_core/renderer/command/effect/light_limiter.h"
#include "audio_core/renderer/command/effect/multi_tap_biquad_filter.h"
#include "audio_core/renderer/command/effect/reverb.h"
#include "audio_core/renderer/command/icommand.h"
#include "audio_core/renderer/command/mix/clear_mix.h"
#include "audio_core/renderer/command/mix/copy_mix.h"
#include "audio_core/renderer/command/mix/depop_for_mix_buffers.h"
#include "audio_core/renderer/command/mix/depop_prepare.h"
#include "audio_core/renderer/command/mix/mix.h"
#include "audio_core/renderer/command/mix/mix_ramp.h"
#include "audio_core/renderer/command/mix/mix_ramp_grouped.h"
#include "audio_core/renderer/command/mix/volume.h"
#include "audio_core/renderer/command/mix/volume_ramp.h"
#include "audio_core/renderer/command/performance/performance.h"
#include "audio_core/renderer/command/resample/downmix_6ch_to_2ch.h"
#include "audio_core/renderer/command/resample/upsample.h"
#include "audio_core/renderer/command/sink/circular_buffer.h"
#include "audio_core/renderer/command/sink/device.h"

View File

@@ -0,0 +1,90 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <span>
#include "audio_core/adsp/apps/audio_renderer/command_list_processor.h"
#include "audio_core/renderer/command/data_source/adpcm.h"
#include "audio_core/renderer/command/data_source/decode.h"
namespace AudioCore::Renderer {
void AdpcmDataSourceVersion1Command::Dump(const AudioRenderer::CommandListProcessor& processor,
std::string& string) {
string += fmt::format("AdpcmDataSourceVersion1Command\n\toutput_index {:02X} source sample "
"rate {} target sample rate {} src quality {}\n",
output_index, sample_rate, processor.target_sample_rate, src_quality);
}
void AdpcmDataSourceVersion1Command::Process(const AudioRenderer::CommandListProcessor& processor) {
auto out_buffer{processor.mix_buffers.subspan(output_index * processor.sample_count,
processor.sample_count)};
for (auto& wave_buffer : wave_buffers) {
wave_buffer.loop_start_offset = wave_buffer.start_offset;
wave_buffer.loop_end_offset = wave_buffer.end_offset;
wave_buffer.loop_count = wave_buffer.loop ? -1 : 0;
}
DecodeFromWaveBuffersArgs args{
.sample_format{SampleFormat::Adpcm},
.output{out_buffer},
.voice_state{reinterpret_cast<VoiceState*>(voice_state)},
.wave_buffers{wave_buffers},
.channel{0},
.channel_count{1},
.src_quality{src_quality},
.pitch{pitch},
.source_sample_rate{sample_rate},
.target_sample_rate{processor.target_sample_rate},
.sample_count{processor.sample_count},
.data_address{data_address},
.data_size{data_size},
.IsVoicePlayedSampleCountResetAtLoopPointSupported{(flags & 1) != 0},
.IsVoicePitchAndSrcSkippedSupported{(flags & 2) != 0},
};
DecodeFromWaveBuffers(*processor.memory, args);
}
bool AdpcmDataSourceVersion1Command::Verify(const AudioRenderer::CommandListProcessor& processor) {
return true;
}
void AdpcmDataSourceVersion2Command::Dump(const AudioRenderer::CommandListProcessor& processor,
std::string& string) {
string += fmt::format("AdpcmDataSourceVersion2Command\n\toutput_index {:02X} source sample "
"rate {} target sample rate {} src quality {}\n",
output_index, sample_rate, processor.target_sample_rate, src_quality);
}
void AdpcmDataSourceVersion2Command::Process(const AudioRenderer::CommandListProcessor& processor) {
auto out_buffer{processor.mix_buffers.subspan(output_index * processor.sample_count,
processor.sample_count)};
DecodeFromWaveBuffersArgs args{
.sample_format{SampleFormat::Adpcm},
.output{out_buffer},
.voice_state{reinterpret_cast<VoiceState*>(voice_state)},
.wave_buffers{wave_buffers},
.channel{0},
.channel_count{1},
.src_quality{src_quality},
.pitch{pitch},
.source_sample_rate{sample_rate},
.target_sample_rate{processor.target_sample_rate},
.sample_count{processor.sample_count},
.data_address{data_address},
.data_size{data_size},
.IsVoicePlayedSampleCountResetAtLoopPointSupported{(flags & 1) != 0},
.IsVoicePitchAndSrcSkippedSupported{(flags & 2) != 0},
};
DecodeFromWaveBuffers(*processor.memory, args);
}
bool AdpcmDataSourceVersion2Command::Verify(const AudioRenderer::CommandListProcessor& processor) {
return true;
}
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,120 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <string>
#include "audio_core/common/common.h"
#include "audio_core/common/wave_buffer.h"
#include "audio_core/renderer/command/icommand.h"
#include "common/common_types.h"
namespace AudioCore::ADSP::AudioRenderer {
class CommandListProcessor;
}
namespace AudioCore::Renderer {
/**
* AudioRenderer command to decode ADPCM-encoded version 1 wavebuffers
* into the output_index mix buffer.
*/
struct AdpcmDataSourceVersion1Command : 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;
/// Quality used for sample rate conversion
SrcQuality src_quality;
/// Mix buffer index for decoded samples
s16 output_index;
/// Flags to control decoding (see AudioCore::Renderer::VoiceInfo::Flags)
u16 flags;
/// Wavebuffer sample rate
u32 sample_rate;
/// Pitch used for sample rate conversion
f32 pitch;
/// Wavebuffers containing the wavebuffer address, context address, looping information etc
std::array<WaveBufferVersion2, MaxWaveBuffers> wave_buffers;
/// Voice state, updated each call and written back to game
CpuAddr voice_state;
/// Coefficients data address
CpuAddr data_address;
/// Coefficients data size
u64 data_size;
};
/**
* AudioRenderer command to decode ADPCM-encoded version 2 wavebuffers
* into the output_index mix buffer.
*/
struct AdpcmDataSourceVersion2Command : 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;
/// Quality used for sample rate conversion
SrcQuality src_quality;
/// Mix buffer index for decoded samples
s16 output_index;
/// Flags to control decoding (see AudioCore::Renderer::VoiceInfo::Flags)
u16 flags;
/// Wavebuffer sample rate
u32 sample_rate;
/// Pitch used for sample rate conversion
f32 pitch;
/// Target channel to read within the wavebuffer
s8 channel_index;
/// Number of channels within the wavebuffer
s8 channel_count;
/// Wavebuffers containing the wavebuffer address, context address, looping information etc
std::array<WaveBufferVersion2, MaxWaveBuffers> wave_buffers;
/// Voice state, updated each call and written back to game
CpuAddr voice_state;
/// Coefficients data address
CpuAddr data_address;
/// Coefficients data size
u64 data_size;
};
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,429 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <array>
#include <vector>
#include "audio_core/renderer/command/data_source/decode.h"
#include "audio_core/renderer/command/resample/resample.h"
#include "common/fixed_point.h"
#include "common/logging/log.h"
#include "common/scratch_buffer.h"
#include "core/guest_memory.h"
#include "core/memory.h"
namespace AudioCore::Renderer {
constexpr u32 TempBufferSize = 0x3F00;
constexpr std::array<u8, 3> PitchBySrcQuality = {4, 8, 4};
/**
* Decode PCM data. Only s16 or f32 is supported.
*
* @tparam T - Type to decode. Only s16 and f32 are supported.
* @param memory - Core memory for reading samples.
* @param out_buffer - Output mix buffer to receive the samples.
* @param req - Information for how to decode.
* @return Number of samples decoded.
*/
template <typename T>
static u32 DecodePcm(Core::Memory::Memory& memory, std::span<s16> out_buffer,
const DecodeArg& req) {
constexpr s32 min{std::numeric_limits<s16>::min()};
constexpr s32 max{std::numeric_limits<s16>::max()};
if (req.buffer == 0 || req.buffer_size == 0) {
return 0;
}
if (req.start_offset >= req.end_offset) {
return 0;
}
auto samples_to_decode{
std::min(req.samples_to_read, req.end_offset - req.start_offset - req.offset)};
u32 channel_count{static_cast<u32>(req.channel_count)};
switch (req.channel_count) {
default: {
const VAddr source{req.buffer +
(((req.start_offset + req.offset) * channel_count) * sizeof(T))};
const u64 size{channel_count * samples_to_decode};
Core::Memory::CpuGuestMemory<T, Core::Memory::GuestMemoryFlags::UnsafeRead> samples(
memory, source, size);
if constexpr (std::is_floating_point_v<T>) {
for (u32 i = 0; i < samples_to_decode; i++) {
auto sample{static_cast<s32>(samples[i * channel_count + req.target_channel] *
std::numeric_limits<s16>::max())};
out_buffer[i] = static_cast<s16>(std::clamp(sample, min, max));
}
} else {
for (u32 i = 0; i < samples_to_decode; i++) {
out_buffer[i] = samples[i * channel_count + req.target_channel];
}
}
} break;
case 1:
if (req.target_channel != 0) {
LOG_ERROR(Service_Audio, "Invalid target channel, expected 0, got {}",
req.target_channel);
return 0;
}
const VAddr source{req.buffer + ((req.start_offset + req.offset) * sizeof(T))};
Core::Memory::CpuGuestMemory<T, Core::Memory::GuestMemoryFlags::UnsafeRead> samples(
memory, source, samples_to_decode);
if constexpr (std::is_floating_point_v<T>) {
for (u32 i = 0; i < samples_to_decode; i++) {
auto sample{static_cast<s32>(samples[i * channel_count + req.target_channel] *
std::numeric_limits<s16>::max())};
out_buffer[i] = static_cast<s16>(std::clamp(sample, min, max));
}
} else {
std::memcpy(out_buffer.data(), samples.data(), samples_to_decode * sizeof(s16));
}
break;
}
return samples_to_decode;
}
/**
* Decode ADPCM data.
*
* @param memory - Core memory for reading samples.
* @param out_buffer - Output mix buffer to receive the samples.
* @param req - Information for how to decode.
* @return Number of samples decoded.
*/
static u32 DecodeAdpcm(Core::Memory::Memory& memory, std::span<s16> out_buffer,
const DecodeArg& req) {
constexpr u32 SamplesPerFrame{14};
constexpr u32 NibblesPerFrame{16};
if (req.buffer == 0 || req.buffer_size == 0) {
return 0;
}
if (req.end_offset < req.start_offset) {
return 0;
}
auto end{(req.end_offset % SamplesPerFrame) +
NibblesPerFrame * (req.end_offset / SamplesPerFrame)};
if (req.end_offset % SamplesPerFrame) {
end += 3;
} else {
end += 1;
}
if (req.buffer_size < end / 2) {
return 0;
}
auto start_pos{req.start_offset + req.offset};
auto samples_to_process{std::min(req.end_offset - start_pos, req.samples_to_read)};
if (samples_to_process == 0) {
return 0;
}
auto samples_to_read{samples_to_process};
auto samples_remaining_in_frame{start_pos % SamplesPerFrame};
auto position_in_frame{(start_pos / SamplesPerFrame) * NibblesPerFrame +
samples_remaining_in_frame};
if (samples_remaining_in_frame) {
position_in_frame += 2;
}
const auto size{std::max((samples_to_process / 8U) * SamplesPerFrame, 8U)};
Core::Memory::CpuGuestMemory<u8, Core::Memory::GuestMemoryFlags::UnsafeRead> wavebuffer(
memory, req.buffer + position_in_frame / 2, size);
auto context{req.adpcm_context};
auto header{context->header};
u8 coeff_index{static_cast<u8>((header >> 4U) & 0xFU)};
u8 scale{static_cast<u8>(header & 0xFU)};
s32 coeff0{req.coefficients[coeff_index * 2 + 0]};
s32 coeff1{req.coefficients[coeff_index * 2 + 1]};
auto yn0{context->yn0};
auto yn1{context->yn1};
static constexpr std::array<s32, 16> Steps{
0, 1, 2, 3, 4, 5, 6, 7, -8, -7, -6, -5, -4, -3, -2, -1,
};
const auto decode_sample = [&](const s32 code) -> s16 {
const auto xn = code * (1 << scale);
const auto prediction = coeff0 * yn0 + coeff1 * yn1;
const auto sample = ((xn << 11) + 0x400 + prediction) >> 11;
const auto saturated = std::clamp<s32>(sample, -0x8000, 0x7FFF);
yn1 = yn0;
yn0 = static_cast<s16>(saturated);
return yn0;
};
u32 read_index{0};
u32 write_index{0};
while (samples_to_read > 0) {
// Are we at a new frame?
if ((position_in_frame % NibblesPerFrame) == 0) {
header = wavebuffer[read_index++];
coeff_index = (header >> 4) & 0xF;
scale = header & 0xF;
coeff0 = req.coefficients[coeff_index * 2 + 0];
coeff1 = req.coefficients[coeff_index * 2 + 1];
position_in_frame += 2;
// Can we consume all of this frame's samples?
if (samples_to_read >= SamplesPerFrame) {
// Can grab all samples until the next header
for (u32 i = 0; i < SamplesPerFrame / 2; i++) {
auto code0{Steps[(wavebuffer[read_index] >> 4) & 0xF]};
auto code1{Steps[wavebuffer[read_index] & 0xF]};
read_index++;
out_buffer[write_index++] = decode_sample(code0);
out_buffer[write_index++] = decode_sample(code1);
}
position_in_frame += SamplesPerFrame;
samples_to_read -= SamplesPerFrame;
continue;
}
}
// Decode a single sample
auto code{wavebuffer[read_index]};
if (position_in_frame & 1) {
code &= 0xF;
read_index++;
} else {
code >>= 4;
}
out_buffer[write_index++] = decode_sample(Steps[code]);
position_in_frame++;
samples_to_read--;
}
context->header = header;
context->yn0 = yn0;
context->yn1 = yn1;
return samples_to_process;
}
/**
* Decode implementation.
* Decode wavebuffers according to the given args.
*
* @param memory - Core memory to read data from.
* @param args - The wavebuffer data, and information for how to decode it.
*/
void DecodeFromWaveBuffers(Core::Memory::Memory& memory, const DecodeFromWaveBuffersArgs& args) {
static constexpr auto EndWaveBuffer = [](auto& voice_state, auto& wavebuffer, auto& index,
auto& played_samples, auto& consumed) -> void {
voice_state.wave_buffer_valid[index] = false;
voice_state.loop_count = 0;
if (wavebuffer.stream_ended) {
played_samples = 0;
}
index = (index + 1) % MaxWaveBuffers;
consumed++;
};
auto& voice_state{*args.voice_state};
auto remaining_sample_count{args.sample_count};
auto fraction{voice_state.fraction};
const auto sample_rate_ratio{Common::FixedPoint<49, 15>(
(f32)args.source_sample_rate / (f32)args.target_sample_rate * (f32)args.pitch)};
const auto size_required{fraction + remaining_sample_count * sample_rate_ratio};
if (size_required < 0) {
return;
}
auto pitch{PitchBySrcQuality[static_cast<u32>(args.src_quality)]};
if (static_cast<u32>(pitch + size_required.to_int_floor()) > TempBufferSize) {
return;
}
auto max_remaining_sample_count{
((Common::FixedPoint<17, 15>(TempBufferSize) - fraction) / sample_rate_ratio)
.to_uint_floor()};
max_remaining_sample_count = std::min(max_remaining_sample_count, remaining_sample_count);
auto wavebuffers_consumed{voice_state.wave_buffers_consumed};
auto wavebuffer_index{voice_state.wave_buffer_index};
auto played_sample_count{voice_state.played_sample_count};
bool is_buffer_starved{false};
u32 offset{voice_state.offset};
auto output_buffer{args.output};
std::array<s16, TempBufferSize> temp_buffer{};
while (remaining_sample_count > 0) {
const auto samples_to_write{std::min(remaining_sample_count, max_remaining_sample_count)};
const auto samples_to_read{
(fraction + samples_to_write * sample_rate_ratio).to_uint_floor()};
u32 temp_buffer_pos{0};
if (!args.IsVoicePitchAndSrcSkippedSupported) {
for (u32 i = 0; i < pitch; i++) {
temp_buffer[i] = voice_state.sample_history[i];
}
temp_buffer_pos = pitch;
}
u32 samples_read{0};
while (samples_read < samples_to_read) {
if (wavebuffer_index >= MaxWaveBuffers) {
LOG_ERROR(Service_Audio, "Invalid wavebuffer index! {}", wavebuffer_index);
wavebuffer_index = 0;
voice_state.wave_buffer_valid.fill(false);
wavebuffers_consumed = MaxWaveBuffers;
}
if (!voice_state.wave_buffer_valid[wavebuffer_index]) {
is_buffer_starved = true;
break;
}
auto& wavebuffer{args.wave_buffers[wavebuffer_index]};
if (offset == 0 && args.sample_format == SampleFormat::Adpcm &&
wavebuffer.context != 0) {
memory.ReadBlockUnsafe(wavebuffer.context, &voice_state.adpcm_context,
wavebuffer.context_size);
}
auto start_offset{wavebuffer.start_offset};
auto end_offset{wavebuffer.end_offset};
if (wavebuffer.loop && voice_state.loop_count > 0 &&
wavebuffer.loop_start_offset <= wavebuffer.loop_end_offset) {
start_offset = wavebuffer.loop_start_offset;
end_offset = wavebuffer.loop_end_offset;
}
DecodeArg decode_arg{
.buffer{wavebuffer.buffer},
.buffer_size{wavebuffer.buffer_size},
.start_offset{start_offset},
.end_offset{end_offset},
.channel_count{args.channel_count},
.coefficients{},
.adpcm_context{nullptr},
.target_channel{args.channel},
.offset{offset},
.samples_to_read{samples_to_read - samples_read},
};
s32 samples_decoded{0};
switch (args.sample_format) {
case SampleFormat::PcmInt16:
samples_decoded = DecodePcm<s16>(
memory, {&temp_buffer[temp_buffer_pos], TempBufferSize - temp_buffer_pos},
decode_arg);
break;
case SampleFormat::PcmFloat:
samples_decoded = DecodePcm<f32>(
memory, {&temp_buffer[temp_buffer_pos], TempBufferSize - temp_buffer_pos},
decode_arg);
break;
case SampleFormat::Adpcm: {
decode_arg.adpcm_context = &voice_state.adpcm_context;
memory.ReadBlockUnsafe(args.data_address, &decode_arg.coefficients, args.data_size);
samples_decoded = DecodeAdpcm(
memory, {&temp_buffer[temp_buffer_pos], TempBufferSize - temp_buffer_pos},
decode_arg);
} break;
default:
LOG_ERROR(Service_Audio, "Invalid sample format to decode {}",
static_cast<u32>(args.sample_format));
samples_decoded = 0;
break;
}
played_sample_count += samples_decoded;
samples_read += samples_decoded;
temp_buffer_pos += samples_decoded;
offset += samples_decoded;
if (samples_decoded && offset < end_offset - start_offset) {
continue;
}
offset = 0;
if (wavebuffer.loop) {
voice_state.loop_count++;
if (wavebuffer.loop_count >= 0 &&
(voice_state.loop_count > wavebuffer.loop_count || samples_decoded == 0)) {
EndWaveBuffer(voice_state, wavebuffer, wavebuffer_index, played_sample_count,
wavebuffers_consumed);
}
if (samples_decoded == 0) {
is_buffer_starved = true;
break;
}
if (args.IsVoicePlayedSampleCountResetAtLoopPointSupported) {
played_sample_count = 0;
}
} else {
EndWaveBuffer(voice_state, wavebuffer, wavebuffer_index, played_sample_count,
wavebuffers_consumed);
}
}
if (args.IsVoicePitchAndSrcSkippedSupported) {
if (samples_read > output_buffer.size()) {
LOG_ERROR(Service_Audio, "Attempting to write past the end of output buffer!");
}
for (u32 i = 0; i < samples_read; i++) {
output_buffer[i] = temp_buffer[i];
}
} else {
std::memset(&temp_buffer[temp_buffer_pos], 0,
(samples_to_read - samples_read) * sizeof(s16));
Resample(output_buffer, temp_buffer, sample_rate_ratio, fraction, samples_to_write,
args.src_quality);
std::memcpy(voice_state.sample_history.data(), &temp_buffer[samples_to_read],
pitch * sizeof(s16));
}
remaining_sample_count -= samples_to_write;
if (remaining_sample_count != 0 && is_buffer_starved) {
LOG_ERROR(Service_Audio, "Samples remaining but buffer is starving??");
break;
}
output_buffer = output_buffer.subspan(samples_to_write);
}
voice_state.wave_buffers_consumed = wavebuffers_consumed;
voice_state.played_sample_count = played_sample_count;
voice_state.wave_buffer_index = wavebuffer_index;
voice_state.offset = offset;
voice_state.fraction = fraction;
}
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <array>
#include <span>
#include "audio_core/common/common.h"
#include "audio_core/common/wave_buffer.h"
#include "audio_core/renderer/voice/voice_state.h"
#include "common/common_types.h"
namespace Core::Memory {
class Memory;
}
namespace AudioCore::Renderer {
struct DecodeFromWaveBuffersArgs {
SampleFormat sample_format;
std::span<s32> output;
VoiceState* voice_state;
std::span<WaveBufferVersion2> wave_buffers;
s8 channel;
s8 channel_count;
SrcQuality src_quality;
f32 pitch;
u32 source_sample_rate;
u32 target_sample_rate;
u32 sample_count;
CpuAddr data_address;
u64 data_size;
bool IsVoicePlayedSampleCountResetAtLoopPointSupported;
bool IsVoicePitchAndSrcSkippedSupported;
};
struct DecodeArg {
CpuAddr buffer;
u64 buffer_size;
u32 start_offset;
u32 end_offset;
s8 channel_count;
std::array<s16, 16> coefficients;
VoiceState::AdpcmContext* adpcm_context;
s8 target_channel;
u32 offset;
u32 samples_to_read;
};
/**
* Decode wavebuffers according to the given args.
*
* @param memory - Core memory to read data from.
* @param args - The wavebuffer data, and information for how to decode it.
*/
void DecodeFromWaveBuffers(Core::Memory::Memory& memory, const DecodeFromWaveBuffersArgs& args);
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,96 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/adsp/apps/audio_renderer/command_list_processor.h"
#include "audio_core/renderer/command/data_source/decode.h"
#include "audio_core/renderer/command/data_source/pcm_float.h"
namespace AudioCore::Renderer {
void PcmFloatDataSourceVersion1Command::Dump(const AudioRenderer::CommandListProcessor& processor,
std::string& string) {
string +=
fmt::format("PcmFloatDataSourceVersion1Command\n\toutput_index {:02X} channel {} "
"channel count {} source sample rate {} target sample rate {} src quality {}\n",
output_index, channel_index, channel_count, sample_rate,
processor.target_sample_rate, src_quality);
}
void PcmFloatDataSourceVersion1Command::Process(
const AudioRenderer::CommandListProcessor& processor) {
auto out_buffer = processor.mix_buffers.subspan(output_index * processor.sample_count,
processor.sample_count);
for (auto& wave_buffer : wave_buffers) {
wave_buffer.loop_start_offset = wave_buffer.start_offset;
wave_buffer.loop_end_offset = wave_buffer.end_offset;
wave_buffer.loop_count = wave_buffer.loop ? -1 : 0;
}
DecodeFromWaveBuffersArgs args{
.sample_format{SampleFormat::PcmFloat},
.output{out_buffer},
.voice_state{reinterpret_cast<VoiceState*>(voice_state)},
.wave_buffers{wave_buffers},
.channel{channel_index},
.channel_count{channel_count},
.src_quality{src_quality},
.pitch{pitch},
.source_sample_rate{sample_rate},
.target_sample_rate{processor.target_sample_rate},
.sample_count{processor.sample_count},
.data_address{0},
.data_size{0},
.IsVoicePlayedSampleCountResetAtLoopPointSupported{(flags & 1) != 0},
.IsVoicePitchAndSrcSkippedSupported{(flags & 2) != 0},
};
DecodeFromWaveBuffers(*processor.memory, args);
}
bool PcmFloatDataSourceVersion1Command::Verify(
const AudioRenderer::CommandListProcessor& processor) {
return true;
}
void PcmFloatDataSourceVersion2Command::Dump(const AudioRenderer::CommandListProcessor& processor,
std::string& string) {
string +=
fmt::format("PcmFloatDataSourceVersion2Command\n\toutput_index {:02X} channel {} "
"channel count {} source sample rate {} target sample rate {} src quality {}\n",
output_index, channel_index, channel_count, sample_rate,
processor.target_sample_rate, src_quality);
}
void PcmFloatDataSourceVersion2Command::Process(
const AudioRenderer::CommandListProcessor& processor) {
auto out_buffer = processor.mix_buffers.subspan(output_index * processor.sample_count,
processor.sample_count);
DecodeFromWaveBuffersArgs args{
.sample_format{SampleFormat::PcmFloat},
.output{out_buffer},
.voice_state{reinterpret_cast<VoiceState*>(voice_state)},
.wave_buffers{wave_buffers},
.channel{channel_index},
.channel_count{channel_count},
.src_quality{src_quality},
.pitch{pitch},
.source_sample_rate{sample_rate},
.target_sample_rate{processor.target_sample_rate},
.sample_count{processor.sample_count},
.data_address{0},
.data_size{0},
.IsVoicePlayedSampleCountResetAtLoopPointSupported{(flags & 1) != 0},
.IsVoicePitchAndSrcSkippedSupported{(flags & 2) != 0},
};
DecodeFromWaveBuffers(*processor.memory, args);
}
bool PcmFloatDataSourceVersion2Command::Verify(
const AudioRenderer::CommandListProcessor& processor) {
return true;
}
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,114 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <string>
#include "audio_core/common/wave_buffer.h"
#include "audio_core/renderer/command/icommand.h"
#include "common/common_types.h"
namespace AudioCore::ADSP::AudioRenderer {
class CommandListProcessor;
}
namespace AudioCore::Renderer {
/**
* AudioRenderer command to decode PCM float-encoded version 1 wavebuffers
* into the output_index mix buffer.
*/
struct PcmFloatDataSourceVersion1Command : 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;
/// Quality used for sample rate conversion
SrcQuality src_quality;
/// Mix buffer index for decoded samples
s16 output_index;
/// Flags to control decoding (see AudioCore::Renderer::VoiceInfo::Flags)
u16 flags;
/// Wavebuffer sample rate
u32 sample_rate;
/// Pitch used for sample rate conversion
f32 pitch;
/// Target channel to read within the wavebuffer
s8 channel_index;
/// Number of channels within the wavebuffer
s8 channel_count;
/// Wavebuffers containing the wavebuffer address, context address, looping information etc
std::array<WaveBufferVersion2, MaxWaveBuffers> wave_buffers;
/// Voice state, updated each call and written back to game
CpuAddr voice_state;
};
/**
* AudioRenderer command to decode PCM float-encoded version 2 wavebuffers
* into the output_index mix buffer.
*/
struct PcmFloatDataSourceVersion2Command : 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;
/// Quality used for sample rate conversion
SrcQuality src_quality;
/// Mix buffer index for decoded samples
s16 output_index;
/// Flags to control decoding (see AudioCore::Renderer::VoiceInfo::Flags)
u16 flags;
/// Wavebuffer sample rate
u32 sample_rate;
/// Pitch used for sample rate conversion
f32 pitch;
/// Target channel to read within the wavebuffer
s8 channel_index;
/// Number of channels within the wavebuffer
s8 channel_count;
/// Wavebuffers containing the wavebuffer address, context address, looping information etc
std::array<WaveBufferVersion2, MaxWaveBuffers> wave_buffers;
/// Voice state, updated each call and written back to game
CpuAddr voice_state;
};
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,97 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <span>
#include "audio_core/adsp/apps/audio_renderer/command_list_processor.h"
#include "audio_core/renderer/command/data_source/decode.h"
#include "audio_core/renderer/command/data_source/pcm_int16.h"
namespace AudioCore::Renderer {
void PcmInt16DataSourceVersion1Command::Dump(const AudioRenderer::CommandListProcessor& processor,
std::string& string) {
string +=
fmt::format("PcmInt16DataSourceVersion1Command\n\toutput_index {:02X} channel {} "
"channel count {} source sample rate {} target sample rate {} src quality {}\n",
output_index, channel_index, channel_count, sample_rate,
processor.target_sample_rate, src_quality);
}
void PcmInt16DataSourceVersion1Command::Process(
const AudioRenderer::CommandListProcessor& processor) {
auto out_buffer = processor.mix_buffers.subspan(output_index * processor.sample_count,
processor.sample_count);
for (auto& wave_buffer : wave_buffers) {
wave_buffer.loop_start_offset = wave_buffer.start_offset;
wave_buffer.loop_end_offset = wave_buffer.end_offset;
wave_buffer.loop_count = wave_buffer.loop ? -1 : 0;
}
DecodeFromWaveBuffersArgs args{
.sample_format{SampleFormat::PcmInt16},
.output{out_buffer},
.voice_state{reinterpret_cast<VoiceState*>(voice_state)},
.wave_buffers{wave_buffers},
.channel{channel_index},
.channel_count{channel_count},
.src_quality{src_quality},
.pitch{pitch},
.source_sample_rate{sample_rate},
.target_sample_rate{processor.target_sample_rate},
.sample_count{processor.sample_count},
.data_address{0},
.data_size{0},
.IsVoicePlayedSampleCountResetAtLoopPointSupported{(flags & 1) != 0},
.IsVoicePitchAndSrcSkippedSupported{(flags & 2) != 0},
};
DecodeFromWaveBuffers(*processor.memory, args);
}
bool PcmInt16DataSourceVersion1Command::Verify(
const AudioRenderer::CommandListProcessor& processor) {
return true;
}
void PcmInt16DataSourceVersion2Command::Dump(const AudioRenderer::CommandListProcessor& processor,
std::string& string) {
string +=
fmt::format("PcmInt16DataSourceVersion2Command\n\toutput_index {:02X} channel {} "
"channel count {} source sample rate {} target sample rate {} src quality {}\n",
output_index, channel_index, channel_count, sample_rate,
processor.target_sample_rate, src_quality);
}
void PcmInt16DataSourceVersion2Command::Process(
const AudioRenderer::CommandListProcessor& processor) {
auto out_buffer = processor.mix_buffers.subspan(output_index * processor.sample_count,
processor.sample_count);
DecodeFromWaveBuffersArgs args{
.sample_format{SampleFormat::PcmInt16},
.output{out_buffer},
.voice_state{reinterpret_cast<VoiceState*>(voice_state)},
.wave_buffers{wave_buffers},
.channel{channel_index},
.channel_count{channel_count},
.src_quality{src_quality},
.pitch{pitch},
.source_sample_rate{sample_rate},
.target_sample_rate{processor.target_sample_rate},
.sample_count{processor.sample_count},
.data_address{0},
.data_size{0},
.IsVoicePlayedSampleCountResetAtLoopPointSupported{(flags & 1) != 0},
.IsVoicePitchAndSrcSkippedSupported{(flags & 2) != 0},
};
DecodeFromWaveBuffers(*processor.memory, args);
}
bool PcmInt16DataSourceVersion2Command::Verify(
const AudioRenderer::CommandListProcessor& processor) {
return true;
}
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,111 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <string>
#include "audio_core/common/wave_buffer.h"
#include "audio_core/renderer/command/icommand.h"
#include "common/common_types.h"
namespace AudioCore::ADSP::AudioRenderer {
class CommandListProcessor;
}
namespace AudioCore::Renderer {
/**
* AudioRenderer command to decode PCM s16-encoded version 1 wavebuffers
* into the output_index mix buffer.
*/
struct PcmInt16DataSourceVersion1Command : 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;
/// Quality used for sample rate conversion
SrcQuality src_quality;
/// Mix buffer index for decoded samples
s16 output_index;
/// Flags to control decoding (see AudioCore::Renderer::VoiceInfo::Flags)
u16 flags;
/// Wavebuffer sample rate
u32 sample_rate;
/// Pitch used for sample rate conversion
f32 pitch;
/// Target channel to read within the wavebuffer
s8 channel_index;
/// Number of channels within the wavebuffer
s8 channel_count;
/// Wavebuffers containing the wavebuffer address, context address, looping information etc
std::array<WaveBufferVersion2, MaxWaveBuffers> wave_buffers;
/// Voice state, updated each call and written back to game
CpuAddr voice_state;
};
/**
* AudioRenderer command to decode PCM s16-encoded version 2 wavebuffers
* into the output_index mix buffer.
*/
struct PcmInt16DataSourceVersion2Command : 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;
/// Quality used for sample rate conversion
SrcQuality src_quality;
/// Mix buffer index for decoded samples
s16 output_index;
/// Flags to control decoding (see AudioCore::Renderer::VoiceInfo::Flags)
u16 flags;
/// Wavebuffer sample rate
u32 sample_rate;
/// Pitch used for sample rate conversion
f32 pitch;
/// Target channel to read within the wavebuffer
s8 channel_index;
/// Number of channels within the wavebuffer
s8 channel_count;
/// Wavebuffers containing the wavebuffer address, context address, looping information etc
std::array<WaveBufferVersion2, MaxWaveBuffers> wave_buffers;
/// Voice state, updated each call and written back to game
CpuAddr voice_state;
};
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,215 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/adsp/apps/audio_renderer/command_list_processor.h"
#include "audio_core/renderer/command/effect/aux_.h"
#include "audio_core/renderer/effect/aux_.h"
#include "core/core.h"
#include "core/memory.h"
namespace AudioCore::Renderer {
/**
* Reset an AuxBuffer.
*
* @param memory - Core memory for writing.
* @param aux_info - Memory address pointing to the AuxInfo to reset.
*/
static void ResetAuxBufferDsp(Core::Memory::Memory& memory, const CpuAddr aux_info) {
if (aux_info == 0) {
LOG_ERROR(Service_Audio, "Aux info is 0!");
return;
}
AuxInfo::AuxInfoDsp info{};
memory.ReadBlockUnsafe(aux_info, &info, sizeof(AuxInfo::AuxInfoDsp));
info.read_offset = 0;
info.write_offset = 0;
info.total_sample_count = 0;
memory.WriteBlockUnsafe(aux_info, &info, sizeof(AuxInfo::AuxInfoDsp));
}
/**
* Write the given input mix buffer to the memory at send_buffer, and update send_info_ if
* update_count is set, to notify the game that an update happened.
*
* @param memory - Core memory for writing.
* @param send_info_ - Meta information for where to write the mix buffer.
* @param sample_count - Unused.
* @param send_buffer - Memory address to write the mix buffer to.
* @param count_max - Maximum number of samples in the receiving buffer.
* @param input - Input mix buffer to write.
* @param write_count_ - Number of samples to write.
* @param write_offset - Current offset to begin writing the receiving buffer at.
* @param update_count - If non-zero, send_info_ will be updated.
* @return Number of samples written.
*/
static u32 WriteAuxBufferDsp(Core::Memory::Memory& memory, CpuAddr send_info_,
[[maybe_unused]] u32 sample_count, CpuAddr send_buffer, u32 count_max,
std::span<const s32> input, u32 write_count_, u32 write_offset,
u32 update_count) {
if (write_count_ > count_max) {
LOG_ERROR(Service_Audio,
"write_count must be smaller than count_max! write_count {}, count_max {}",
write_count_, count_max);
return 0;
}
if (send_info_ == 0) {
LOG_ERROR(Service_Audio, "send_info_ is 0!");
return 0;
}
if (input.empty()) {
LOG_ERROR(Service_Audio, "input buffer is empty!");
return 0;
}
if (send_buffer == 0) {
LOG_ERROR(Service_Audio, "send_buffer is 0!");
return 0;
}
if (count_max == 0) {
return 0;
}
AuxInfo::AuxInfoDsp send_info{};
memory.ReadBlockUnsafe(send_info_, &send_info, sizeof(AuxInfo::AuxInfoDsp));
u32 target_write_offset{send_info.write_offset + write_offset};
if (target_write_offset > count_max) {
return 0;
}
u32 write_count{write_count_};
u32 read_pos{0};
while (write_count > 0) {
u32 to_write{std::min(count_max - target_write_offset, write_count)};
if (to_write > 0) {
const auto write_addr = send_buffer + target_write_offset * sizeof(s32);
memory.WriteBlockUnsafe(write_addr, &input[read_pos], to_write * sizeof(s32));
}
target_write_offset = (target_write_offset + to_write) % count_max;
write_count -= to_write;
read_pos += to_write;
}
if (update_count) {
send_info.write_offset = (send_info.write_offset + update_count) % count_max;
}
memory.WriteBlockUnsafe(send_info_, &send_info, sizeof(AuxInfo::AuxInfoDsp));
return write_count_;
}
/**
* Read the given memory at return_buffer into the output mix buffer, and update return_info_ if
* update_count is set, to notify the game that an update happened.
*
* @param memory - Core memory for reading.
* @param return_info_ - Meta information for where to read the mix buffer.
* @param return_buffer - Memory address to read the samples from.
* @param count_max - Maximum number of samples in the receiving buffer.
* @param output - Output mix buffer which will receive the samples.
* @param count_ - Number of samples to read.
* @param read_offset - Current offset to begin reading the return_buffer at.
* @param update_count - If non-zero, send_info_ will be updated.
* @return Number of samples read.
*/
static u32 ReadAuxBufferDsp(Core::Memory::Memory& memory, CpuAddr return_info_,
CpuAddr return_buffer, u32 count_max, std::span<s32> output,
u32 read_count_, u32 read_offset, u32 update_count) {
if (count_max == 0) {
return 0;
}
if (read_count_ > count_max) {
LOG_ERROR(Service_Audio, "count must be smaller than count_max! count {}, count_max {}",
read_count_, count_max);
return 0;
}
if (return_info_ == 0) {
LOG_ERROR(Service_Audio, "return_info_ is 0!");
return 0;
}
if (output.empty()) {
LOG_ERROR(Service_Audio, "output buffer is empty!");
return 0;
}
if (return_buffer == 0) {
LOG_ERROR(Service_Audio, "return_buffer is 0!");
return 0;
}
AuxInfo::AuxInfoDsp return_info{};
memory.ReadBlockUnsafe(return_info_, &return_info, sizeof(AuxInfo::AuxInfoDsp));
u32 target_read_offset{return_info.read_offset + read_offset};
if (target_read_offset > count_max) {
return 0;
}
u32 read_count{read_count_};
u32 write_pos{0};
while (read_count > 0) {
u32 to_read{std::min(count_max - target_read_offset, read_count)};
if (to_read > 0) {
const auto read_addr = return_buffer + target_read_offset * sizeof(s32);
memory.ReadBlockUnsafe(read_addr, &output[write_pos], to_read * sizeof(s32));
}
target_read_offset = (target_read_offset + to_read) % count_max;
read_count -= to_read;
write_pos += to_read;
}
if (update_count) {
return_info.read_offset = (return_info.read_offset + update_count) % count_max;
}
memory.WriteBlockUnsafe(return_info_, &return_info, sizeof(AuxInfo::AuxInfoDsp));
return read_count_;
}
void AuxCommand::Dump([[maybe_unused]] const AudioRenderer::CommandListProcessor& processor,
std::string& string) {
string += fmt::format("AuxCommand\n\tenabled {} input {:02X} output {:02X}\n", effect_enabled,
input, output);
}
void AuxCommand::Process(const AudioRenderer::CommandListProcessor& processor) {
auto input_buffer{
processor.mix_buffers.subspan(input * processor.sample_count, processor.sample_count)};
auto output_buffer{
processor.mix_buffers.subspan(output * processor.sample_count, processor.sample_count)};
if (effect_enabled) {
WriteAuxBufferDsp(*processor.memory, send_buffer_info, processor.sample_count, send_buffer,
count_max, input_buffer, processor.sample_count, write_offset,
update_count);
auto read{ReadAuxBufferDsp(*processor.memory, return_buffer_info, return_buffer, count_max,
output_buffer, processor.sample_count, write_offset,
update_count)};
if (read != processor.sample_count) {
std::memset(&output_buffer[read], 0, (processor.sample_count - read) * sizeof(s32));
}
} else {
ResetAuxBufferDsp(*processor.memory, send_buffer_info);
ResetAuxBufferDsp(*processor.memory, return_buffer_info);
if (input != output) {
std::memcpy(output_buffer.data(), input_buffer.data(), output_buffer.size_bytes());
}
}
}
bool AuxCommand::Verify(const AudioRenderer::CommandListProcessor& processor) {
return true;
}
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,67 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <string>
#include "audio_core/renderer/command/icommand.h"
#include "common/common_types.h"
namespace AudioCore::ADSP::AudioRenderer {
class CommandListProcessor;
}
namespace AudioCore::Renderer {
/**
* AudioRenderer command to read and write an auxiliary buffer, writing the input mix buffer to game
* memory, and reading into the output buffer from game memory.
*/
struct AuxCommand : ICommand {
/**
* Print this command's information to a string.
*
* @param processor - The CommandListProcessor processing this command.
* @param string - The string to print into.
*/
void Dump(const AudioRenderer::CommandListProcessor& processor, std::string& string) override;
/**
* Process this command.
*
* @param processor - The CommandListProcessor processing this command.
*/
void Process(const AudioRenderer::CommandListProcessor& processor) override;
/**
* Verify this command's data is valid.
*
* @param processor - The CommandListProcessor processing this command.
* @return True if the command is valid, otherwise false.
*/
bool Verify(const AudioRenderer::CommandListProcessor& processor) override;
/// Input mix buffer index
s16 input;
/// Output mix buffer index
s16 output;
/// Meta info for writing
CpuAddr send_buffer_info;
/// Meta info for reading
CpuAddr return_buffer_info;
/// Game memory write buffer
CpuAddr send_buffer;
/// Game memory read buffer
CpuAddr return_buffer;
/// Max samples to read/write
u32 count_max;
/// Current read/write offset
u32 write_offset;
/// Number of samples to update per call
u32 update_count;
/// is this effect enabled?
bool effect_enabled;
};
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,110 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/adsp/apps/audio_renderer/command_list_processor.h"
#include "audio_core/renderer/command/effect/biquad_filter.h"
#include "audio_core/renderer/voice/voice_state.h"
#include "common/bit_cast.h"
namespace AudioCore::Renderer {
/**
* Biquad filter float implementation.
*
* @param output - Output container for filtered samples.
* @param input - Input container for samples to be filtered.
* @param b - Feedforward coefficients.
* @param a - Feedback coefficients.
* @param state - State to track previous samples between calls.
* @param sample_count - Number of samples to process.
*/
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) {
constexpr f64 min{std::numeric_limits<s32>::min()};
constexpr f64 max{std::numeric_limits<s32>::max()};
std::array<f64, 3> b{Common::FixedPoint<50, 14>::from_base(b_[0]).to_double(),
Common::FixedPoint<50, 14>::from_base(b_[1]).to_double(),
Common::FixedPoint<50, 14>::from_base(b_[2]).to_double()};
std::array<f64, 2> a{Common::FixedPoint<50, 14>::from_base(a_[0]).to_double(),
Common::FixedPoint<50, 14>::from_base(a_[1]).to_double()};
std::array<f64, 4> s{Common::BitCast<f64>(state.s0), Common::BitCast<f64>(state.s1),
Common::BitCast<f64>(state.s2), Common::BitCast<f64>(state.s3)};
for (u32 i = 0; i < sample_count; i++) {
f64 in_sample{static_cast<f64>(input[i])};
auto sample{in_sample * b[0] + s[0] * b[1] + s[1] * b[2] + s[2] * a[0] + s[3] * a[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.
*
* @param output - Output container for filtered samples.
* @param input - Input container for samples to be filtered.
* @param b - Feedforward coefficients.
* @param a - Feedback coefficients.
* @param state - State to track previous samples between calls.
* @param sample_count - Number of samples to process.
*/
static void ApplyBiquadFilterInt(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) {
constexpr s64 min{std::numeric_limits<s32>::min()};
constexpr s64 max{std::numeric_limits<s32>::max()};
for (u32 i = 0; i < sample_count; i++) {
const s64 in_sample{input[i]};
const s64 sample{in_sample * b[0] + state.s0};
const s64 out_sample{std::clamp<s64>((sample + (1 << 13)) >> 14, min, max)};
output[i] = static_cast<s32>(out_sample);
state.s0 = state.s1 + b[1] * in_sample + a[0] * out_sample;
state.s1 = b[2] * in_sample + a[1] * out_sample;
}
}
void BiquadFilterCommand::Dump(
[[maybe_unused]] const AudioRenderer::CommandListProcessor& processor, std::string& string) {
string += fmt::format(
"BiquadFilterCommand\n\tinput {:02X} output {:02X} needs_init {} use_float_processing {}\n",
input, output, needs_init, use_float_processing);
}
void BiquadFilterCommand::Process(const AudioRenderer::CommandListProcessor& processor) {
auto state_{reinterpret_cast<VoiceState::BiquadFilterState*>(state)};
if (needs_init) {
*state_ = {};
}
auto input_buffer{
processor.mix_buffers.subspan(input * processor.sample_count, processor.sample_count)};
auto output_buffer{
processor.mix_buffers.subspan(output * processor.sample_count, processor.sample_count)};
if (use_float_processing) {
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);
}
}
bool BiquadFilterCommand::Verify(const AudioRenderer::CommandListProcessor& processor) {
return true;
}
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <string>
#include "audio_core/renderer/command/icommand.h"
#include "audio_core/renderer/voice/voice_info.h"
#include "audio_core/renderer/voice/voice_state.h"
#include "common/common_types.h"
namespace AudioCore::ADSP::AudioRenderer {
class CommandListProcessor;
}
namespace AudioCore::Renderer {
/**
* AudioRenderer command for applying a biquad filter to the input mix buffer, saving the results to
* the output mix buffer.
*/
struct BiquadFilterCommand : ICommand {
/**
* Print this command's information to a string.
*
* @param processor - The CommandListProcessor processing this command.
* @param string - The string to print into.
*/
void Dump(const AudioRenderer::CommandListProcessor& processor, std::string& string) override;
/**
* Process this command.
*
* @param processor - The CommandListProcessor processing this command.
*/
void Process(const AudioRenderer::CommandListProcessor& processor) override;
/**
* Verify this command's data is valid.
*
* @param processor - The CommandListProcessor processing this command.
* @return True if the command is valid, otherwise false.
*/
bool Verify(const AudioRenderer::CommandListProcessor& processor) override;
/// Input mix buffer index
s16 input;
/// Output mix buffer index
s16 output;
/// Input parameters for biquad
VoiceInfo::BiquadFilterParameter biquad;
/// 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;
};
/**
* Biquad filter float implementation.
*
* @param output - Output container for filtered samples.
* @param input - Input container for samples to be filtered.
* @param b - Feedforward coefficients.
* @param a - Feedback coefficients.
* @param state - State to track previous samples.
* @param sample_count - Number of samples to process.
*/
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);
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,142 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/adsp/apps/audio_renderer/command_list_processor.h"
#include "audio_core/renderer/command/effect/capture.h"
#include "audio_core/renderer/effect/aux_.h"
#include "core/memory.h"
namespace AudioCore::Renderer {
/**
* Reset an AuxBuffer.
*
* @param memory - Core memory for writing.
* @param aux_info - Memory address pointing to the AuxInfo to reset.
*/
static void ResetAuxBufferDsp(Core::Memory::Memory& memory, const CpuAddr aux_info) {
if (aux_info == 0) {
LOG_ERROR(Service_Audio, "Aux info is 0!");
return;
}
memory.Write32(VAddr(aux_info + offsetof(AuxInfo::AuxInfoDsp, read_offset)), 0);
memory.Write32(VAddr(aux_info + offsetof(AuxInfo::AuxInfoDsp, write_offset)), 0);
memory.Write32(VAddr(aux_info + offsetof(AuxInfo::AuxInfoDsp, total_sample_count)), 0);
}
/**
* Write the given input mix buffer to the memory at send_buffer, and update send_info_ if
* update_count is set, to notify the game that an update happened.
*
* @param memory - Core memory for writing.
* @param send_info_ - Header information for where to write the mix buffer.
* @param send_buffer - Memory address to write the mix buffer to.
* @param count_max - Maximum number of samples in the receiving buffer.
* @param input - Input mix buffer to write.
* @param write_count_ - Number of samples to write.
* @param write_offset - Current offset to begin writing the receiving buffer at.
* @param update_count - If non-zero, send_info_ will be updated.
* @return Number of samples written.
*/
static u32 WriteAuxBufferDsp(Core::Memory::Memory& memory, const CpuAddr send_info_,
const CpuAddr send_buffer, u32 count_max, std::span<const s32> input,
const u32 write_count_, const u32 write_offset,
const u32 update_count) {
if (write_count_ > count_max) {
LOG_ERROR(Service_Audio,
"write_count must be smaller than count_max! write_count {}, count_max {}",
write_count_, count_max);
return 0;
}
if (send_info_ == 0) {
LOG_ERROR(Service_Audio, "send_info is 0!");
return 0;
}
if (input.empty()) {
LOG_ERROR(Service_Audio, "input buffer is empty!");
return 0;
}
if (send_buffer == 0) {
LOG_ERROR(Service_Audio, "send_buffer is 0!");
return 0;
}
if (count_max == 0) {
return 0;
}
AuxInfo::AuxBufferInfo send_info{};
memory.ReadBlockUnsafe(send_info_, &send_info, sizeof(AuxInfo::AuxBufferInfo));
u32 target_write_offset{send_info.dsp_info.write_offset + write_offset};
if (target_write_offset > count_max || write_count_ == 0) {
return 0;
}
u32 write_count{write_count_};
u32 write_pos{0};
while (write_count > 0) {
u32 to_write{std::min(count_max - target_write_offset, write_count)};
if (to_write > 0) {
memory.WriteBlockUnsafe(send_buffer + target_write_offset * sizeof(s32),
&input[write_pos], to_write * sizeof(s32));
}
target_write_offset = (target_write_offset + to_write) % count_max;
write_count -= to_write;
write_pos += to_write;
}
if (update_count) {
const auto count_diff{send_info.dsp_info.total_sample_count -
send_info.cpu_info.total_sample_count};
if (count_diff >= count_max) {
auto dsp_lost_count{send_info.dsp_info.lost_sample_count + update_count};
if (dsp_lost_count - send_info.cpu_info.lost_sample_count <
send_info.dsp_info.lost_sample_count - send_info.cpu_info.lost_sample_count) {
dsp_lost_count = send_info.cpu_info.lost_sample_count - 1;
}
send_info.dsp_info.lost_sample_count = dsp_lost_count;
}
send_info.dsp_info.write_offset =
(send_info.dsp_info.write_offset + update_count + count_max) % count_max;
auto new_sample_count{send_info.dsp_info.total_sample_count + update_count};
if (new_sample_count - send_info.cpu_info.total_sample_count < count_diff) {
new_sample_count = send_info.cpu_info.total_sample_count - 1;
}
send_info.dsp_info.total_sample_count = new_sample_count;
}
memory.WriteBlockUnsafe(send_info_, &send_info, sizeof(AuxInfo::AuxBufferInfo));
return write_count_;
}
void CaptureCommand::Dump([[maybe_unused]] const AudioRenderer::CommandListProcessor& processor,
std::string& string) {
string += fmt::format("CaptureCommand\n\tenabled {} input {:02X} output {:02X}", effect_enabled,
input, output);
}
void CaptureCommand::Process(const AudioRenderer::CommandListProcessor& processor) {
if (effect_enabled) {
auto input_buffer{
processor.mix_buffers.subspan(input * processor.sample_count, processor.sample_count)};
WriteAuxBufferDsp(*processor.memory, send_buffer_info, send_buffer, count_max, input_buffer,
processor.sample_count, write_offset, update_count);
} else {
ResetAuxBufferDsp(*processor.memory, send_buffer_info);
}
}
bool CaptureCommand::Verify(const AudioRenderer::CommandListProcessor& processor) {
return true;
}
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,63 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <string>
#include "audio_core/renderer/command/icommand.h"
#include "common/common_types.h"
namespace AudioCore::ADSP::AudioRenderer {
class CommandListProcessor;
}
namespace AudioCore::Renderer {
/**
* AudioRenderer command for capturing a mix buffer. That is, writing it back to a given game memory
* address.
*/
struct CaptureCommand : ICommand {
/**
* Print this command's information to a string.
*
* @param processor - The CommandListProcessor processing this command.
* @param string - The string to print into.
*/
void Dump(const AudioRenderer::CommandListProcessor& processor, std::string& string) override;
/**
* Process this command.
*
* @param processor - The CommandListProcessor processing this command.
*/
void Process(const AudioRenderer::CommandListProcessor& processor) override;
/**
* Verify this command's data is valid.
*
* @param processor - The CommandListProcessor processing this command.
* @return True if the command is valid, otherwise false.
*/
bool Verify(const AudioRenderer::CommandListProcessor& processor) override;
/// Input mix buffer index
s16 input;
/// Output mix buffer index
s16 output;
/// Meta info for writing
CpuAddr send_buffer_info;
/// Game memory write buffer
CpuAddr send_buffer;
/// Max samples to read/write
u32 count_max;
/// Current read/write offset
u32 write_offset;
/// Number of samples to update per call
u32 update_count;
/// is this effect enabled?
bool effect_enabled;
};
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,155 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cmath>
#include <span>
#include <vector>
#include "audio_core/adsp/apps/audio_renderer/command_list_processor.h"
#include "audio_core/renderer/command/effect/compressor.h"
#include "audio_core/renderer/effect/compressor.h"
namespace AudioCore::Renderer {
static void SetCompressorEffectParameter(const CompressorInfo::ParameterVersion2& params,
CompressorInfo::State& state) {
const auto ratio{1.0f / params.compressor_ratio};
auto makeup_gain{0.0f};
if (params.makeup_gain_enabled) {
makeup_gain = (params.threshold * 0.5f) * (ratio - 1.0f) - 3.0f;
}
state.makeup_gain = makeup_gain;
state.unk_18 = params.unk_28;
const auto a{(params.out_gain + makeup_gain) / 20.0f * 3.3219f};
const auto b{(a - std::trunc(a)) * 0.69315f};
const auto c{std::pow(2.0f, b)};
state.unk_0C = (1.0f - ratio) / 6.0f;
state.unk_14 = params.threshold + 1.5f;
state.unk_10 = params.threshold - 1.5f;
state.unk_20 = c;
}
static void InitializeCompressorEffect(const CompressorInfo::ParameterVersion2& params,
CompressorInfo::State& state) {
state = {};
state.unk_00 = 0;
state.unk_04 = 1.0f;
state.unk_08 = 1.0f;
SetCompressorEffectParameter(params, state);
}
static void ApplyCompressorEffect(const CompressorInfo::ParameterVersion2& params,
CompressorInfo::State& state, bool enabled,
std::span<std::span<const s32>> input_buffers,
std::span<std::span<s32>> output_buffers, u32 sample_count) {
if (enabled) {
auto state_00{state.unk_00};
auto state_04{state.unk_04};
auto state_08{state.unk_08};
auto state_18{state.unk_18};
for (u32 i = 0; i < sample_count; i++) {
auto a{0.0f};
for (s16 channel = 0; channel < params.channel_count; channel++) {
const auto input_sample{Common::FixedPoint<49, 15>(input_buffers[channel][i])};
a += (input_sample * input_sample).to_float();
}
state_00 += params.unk_24 * ((a / params.channel_count) - state.unk_00);
auto b{-100.0f};
auto c{0.0f};
if (state_00 >= 1.0e-10) {
b = std::log10(state_00) * 10.0f;
c = 1.0f;
}
if (b >= state.unk_10) {
const auto d{b >= state.unk_14
? ((1.0f / params.compressor_ratio) - 1.0f) *
(b - params.threshold)
: (b - state.unk_10) * (b - state.unk_10) * -state.unk_0C};
const auto e{d / 20.0f * 3.3219f};
const auto f{(e - std::trunc(e)) * 0.69315f};
c = std::pow(2.0f, f);
}
state_18 = params.unk_28;
auto tmp{c};
if ((state_04 - c) <= 0.08f) {
state_18 = params.unk_2C;
if (((state_04 - c) >= -0.08f) && (std::abs(state_08 - c) >= 0.001f)) {
tmp = state_04;
}
}
state_04 = tmp;
state_08 += (c - state_08) * state_18;
for (s16 channel = 0; channel < params.channel_count; channel++) {
output_buffers[channel][i] = static_cast<s32>(
static_cast<f32>(input_buffers[channel][i]) * state_08 * state.unk_20);
}
}
state.unk_00 = state_00;
state.unk_04 = state_04;
state.unk_08 = state_08;
state.unk_18 = state_18;
} else {
for (s16 channel = 0; channel < params.channel_count; channel++) {
if (params.inputs[channel] != params.outputs[channel]) {
std::memcpy(output_buffers[channel].data(), input_buffers[channel].data(),
output_buffers[channel].size_bytes());
}
}
}
}
void CompressorCommand::Dump([[maybe_unused]] const AudioRenderer::CommandListProcessor& processor,
std::string& string) {
string += fmt::format("CompressorCommand\n\tenabled {} \n\tinputs: ", effect_enabled);
for (s16 i = 0; i < parameter.channel_count; i++) {
string += fmt::format("{:02X}, ", inputs[i]);
}
string += "\n\toutputs: ";
for (s16 i = 0; i < parameter.channel_count; i++) {
string += fmt::format("{:02X}, ", outputs[i]);
}
string += "\n";
}
void CompressorCommand::Process(const AudioRenderer::CommandListProcessor& processor) {
std::array<std::span<const s32>, MaxChannels> input_buffers{};
std::array<std::span<s32>, MaxChannels> output_buffers{};
for (s16 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_{reinterpret_cast<CompressorInfo::State*>(state)};
if (effect_enabled) {
if (parameter.state == CompressorInfo::ParameterState::Updating) {
SetCompressorEffectParameter(parameter, *state_);
} else if (parameter.state == CompressorInfo::ParameterState::Initialized) {
InitializeCompressorEffect(parameter, *state_);
}
}
ApplyCompressorEffect(parameter, *state_, effect_enabled, input_buffers, output_buffers,
processor.sample_count);
}
bool CompressorCommand::Verify(const AudioRenderer::CommandListProcessor& processor) {
return true;
}
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu 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/compressor.h"
#include "common/common_types.h"
namespace AudioCore::ADSP::AudioRenderer {
class CommandListProcessor;
}
namespace AudioCore::Renderer {
/**
* AudioRenderer command for limiting volume between a high and low threshold.
* Version 1.
*/
struct CompressorCommand : 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
CompressorInfo::ParameterVersion2 parameter;
/// State, updated each call
CpuAddr state;
/// Game-supplied workbuffer (Unused)
CpuAddr workbuffer;
/// Is this effect enabled?
bool effect_enabled;
};
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,238 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/adsp/apps/audio_renderer/command_list_processor.h"
#include "audio_core/renderer/command/effect/delay.h"
namespace AudioCore::Renderer {
/**
* Update the DelayInfo state according to the given parameters.
*
* @param params - Input parameters to update the state.
* @param state - State to be updated.
*/
static void SetDelayEffectParameter(const DelayInfo::ParameterVersion1& params,
DelayInfo::State& state) {
auto channel_spread{params.channel_spread};
state.feedback_gain = params.feedback_gain * 0.97998046875f;
state.delay_feedback_gain = state.feedback_gain * (1.0f - channel_spread);
if (params.channel_count == 4 || params.channel_count == 6) {
channel_spread >>= 1;
}
state.delay_feedback_cross_gain = channel_spread * state.feedback_gain;
state.lowpass_feedback_gain = params.lowpass_amount * 0.949951171875f;
state.lowpass_gain = 1.0f - state.lowpass_feedback_gain;
}
/**
* Initialize a new DelayInfo state according to the given parameters.
*
* @param params - Input parameters to update the state.
* @param state - State to be updated.
* @param workbuffer - Game-supplied memory for the state. (Unused)
*/
static void InitializeDelayEffect(const DelayInfo::ParameterVersion1& params,
DelayInfo::State& state,
[[maybe_unused]] const CpuAddr workbuffer) {
state = {};
for (u32 channel = 0; channel < params.channel_count; channel++) {
Common::FixedPoint<32, 32> sample_count_max{0.064f};
sample_count_max *= params.sample_rate.to_int_floor() * params.delay_time_max;
Common::FixedPoint<18, 14> delay_time{params.delay_time};
delay_time *= params.sample_rate / 1000;
Common::FixedPoint<32, 32> sample_count{delay_time};
if (sample_count > sample_count_max) {
sample_count = sample_count_max;
}
state.delay_lines[channel].sample_count_max = sample_count_max.to_int_floor();
state.delay_lines[channel].sample_count = sample_count.to_int_floor();
state.delay_lines[channel].buffer.resize(state.delay_lines[channel].sample_count, 0);
if (state.delay_lines[channel].sample_count == 0) {
state.delay_lines[channel].buffer.push_back(0);
}
state.delay_lines[channel].buffer_pos = 0;
state.delay_lines[channel].decay_rate = 1.0f;
}
SetDelayEffectParameter(params, state);
}
/**
* Delay effect impl, according to the parameters and current state, on the input mix buffers,
* saving the results to the output mix buffers.
*
* @tparam NumChannels - Number of channels to process. 1-6.
* @param params - Input parameters to use.
* @param state - State to use, must be initialized (see InitializeDelayEffect).
* @param inputs - Input mix buffers to performan the delay on.
* @param outputs - Output mix buffers to receive the delayed samples.
* @param sample_count - Number of samples to process.
*/
template <size_t NumChannels>
static void ApplyDelay(const DelayInfo::ParameterVersion1& params, DelayInfo::State& state,
std::span<std::span<const s32>> inputs, std::span<std::span<s32>> outputs,
const u32 sample_count) {
for (u32 sample_index = 0; sample_index < sample_count; sample_index++) {
std::array<Common::FixedPoint<50, 14>, NumChannels> input_samples{};
for (u32 channel = 0; channel < NumChannels; channel++) {
input_samples[channel] = inputs[channel][sample_index] * 64;
}
std::array<Common::FixedPoint<50, 14>, NumChannels> delay_samples{};
for (u32 channel = 0; channel < NumChannels; channel++) {
delay_samples[channel] = state.delay_lines[channel].Read();
}
// clang-format off
std::array<std::array<Common::FixedPoint<18, 14>, NumChannels>, NumChannels> matrix{};
if constexpr (NumChannels == 1) {
matrix = {{
{state.feedback_gain},
}};
} else if constexpr (NumChannels == 2) {
matrix = {{
{state.delay_feedback_gain, state.delay_feedback_cross_gain},
{state.delay_feedback_cross_gain, state.delay_feedback_gain},
}};
} else if constexpr (NumChannels == 4) {
matrix = {{
{state.delay_feedback_gain, state.delay_feedback_cross_gain, state.delay_feedback_cross_gain, 0.0f},
{state.delay_feedback_cross_gain, state.delay_feedback_gain, 0.0f, state.delay_feedback_cross_gain},
{state.delay_feedback_cross_gain, 0.0f, state.delay_feedback_gain, state.delay_feedback_cross_gain},
{0.0f, state.delay_feedback_cross_gain, state.delay_feedback_cross_gain, state.delay_feedback_gain},
}};
} else if constexpr (NumChannels == 6) {
matrix = {{
{state.delay_feedback_gain, 0.0f, state.delay_feedback_cross_gain, 0.0f, state.delay_feedback_cross_gain, 0.0f},
{0.0f, state.delay_feedback_gain, state.delay_feedback_cross_gain, 0.0f, 0.0f, state.delay_feedback_cross_gain},
{state.delay_feedback_cross_gain, state.delay_feedback_cross_gain, state.delay_feedback_gain, 0.0f, 0.0f, 0.0f},
{0.0f, 0.0f, 0.0f, params.feedback_gain, 0.0f, 0.0f},
{state.delay_feedback_cross_gain, 0.0f, 0.0f, 0.0f, state.delay_feedback_gain, state.delay_feedback_cross_gain},
{0.0f, state.delay_feedback_cross_gain, 0.0f, 0.0f, state.delay_feedback_cross_gain, state.delay_feedback_gain},
}};
}
// clang-format on
std::array<Common::FixedPoint<50, 14>, NumChannels> gained_samples{};
for (u32 channel = 0; channel < NumChannels; channel++) {
Common::FixedPoint<50, 14> delay{};
for (u32 j = 0; j < NumChannels; j++) {
delay += delay_samples[j] * matrix[j][channel];
}
gained_samples[channel] = input_samples[channel] * params.in_gain + delay;
}
for (u32 channel = 0; channel < NumChannels; channel++) {
state.lowpass_z[channel] = gained_samples[channel] * state.lowpass_gain +
state.lowpass_z[channel] * state.lowpass_feedback_gain;
state.delay_lines[channel].Write(state.lowpass_z[channel]);
}
for (u32 channel = 0; channel < NumChannels; channel++) {
outputs[channel][sample_index] = (input_samples[channel] * params.dry_gain +
delay_samples[channel] * params.wet_gain)
.to_int_floor() /
64;
}
}
}
/**
* Apply a delay effect if enabled, according to the parameters and current state, on the input mix
* buffers, saving the results to the output mix buffers.
*
* @param params - Input parameters to use.
* @param state - State to use, must be initialized (see InitializeDelayEffect).
* @param enabled - If enabled, delay will be applied, otherwise input is copied to output.
* @param inputs - Input mix buffers to performan the delay on.
* @param outputs - Output mix buffers to receive the delayed samples.
* @param sample_count - Number of samples to process.
*/
static void ApplyDelayEffect(const DelayInfo::ParameterVersion1& params, DelayInfo::State& state,
const bool enabled, std::span<std::span<const s32>> inputs,
std::span<std::span<s32>> outputs, const u32 sample_count) {
if (!IsChannelCountValid(params.channel_count)) {
LOG_ERROR(Service_Audio, "Invalid delay channels {}", params.channel_count);
return;
}
if (enabled) {
switch (params.channel_count) {
case 1:
ApplyDelay<1>(params, state, inputs, outputs, sample_count);
break;
case 2:
ApplyDelay<2>(params, state, inputs, outputs, sample_count);
break;
case 4:
ApplyDelay<4>(params, state, inputs, outputs, sample_count);
break;
case 6:
ApplyDelay<6>(params, state, inputs, outputs, sample_count);
break;
default:
for (u32 channel = 0; channel < params.channel_count; channel++) {
if (inputs[channel].data() != outputs[channel].data()) {
std::memcpy(outputs[channel].data(), inputs[channel].data(),
sample_count * sizeof(s32));
}
}
break;
}
} else {
for (u32 channel = 0; channel < params.channel_count; channel++) {
if (inputs[channel].data() != outputs[channel].data()) {
std::memcpy(outputs[channel].data(), inputs[channel].data(),
sample_count * sizeof(s32));
}
}
}
}
void DelayCommand::Dump([[maybe_unused]] const AudioRenderer::CommandListProcessor& processor,
std::string& string) {
string += fmt::format("DelayCommand\n\tenabled {} \n\tinputs: ", effect_enabled);
for (u32 i = 0; i < MaxChannels; i++) {
string += fmt::format("{:02X}, ", inputs[i]);
}
string += "\n\toutputs: ";
for (u32 i = 0; i < MaxChannels; i++) {
string += fmt::format("{:02X}, ", outputs[i]);
}
string += "\n";
}
void DelayCommand::Process(const AudioRenderer::CommandListProcessor& processor) {
std::array<std::span<const s32>, MaxChannels> input_buffers{};
std::array<std::span<s32>, MaxChannels> output_buffers{};
for (s16 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_{reinterpret_cast<DelayInfo::State*>(state)};
if (effect_enabled) {
if (parameter.state == DelayInfo::ParameterState::Updating) {
SetDelayEffectParameter(parameter, *state_);
} else if (parameter.state == DelayInfo::ParameterState::Initialized) {
InitializeDelayEffect(parameter, *state_, workbuffer);
}
}
ApplyDelayEffect(parameter, *state_, effect_enabled, input_buffers, output_buffers,
processor.sample_count);
}
bool DelayCommand::Verify(const AudioRenderer::CommandListProcessor& processor) {
return true;
}
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu 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/delay.h"
#include "common/common_types.h"
namespace AudioCore::ADSP::AudioRenderer {
class CommandListProcessor;
}
namespace AudioCore::Renderer {
/**
* AudioRenderer command for a delay effect. Delays inputs mix buffers according to the parameters
* and state, outputs receives the delayed samples.
*/
struct DelayCommand : 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
DelayInfo::ParameterVersion1 parameter;
/// State, updated each call
CpuAddr state;
/// Game-supplied workbuffer (Unused)
CpuAddr workbuffer;
/// Is this effect enabled?
bool effect_enabled;
};
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,438 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <numbers>
#include "audio_core/adsp/apps/audio_renderer/command_list_processor.h"
#include "audio_core/renderer/command/effect/i3dl2_reverb.h"
#include "common/polyfill_ranges.h"
namespace AudioCore::Renderer {
constexpr std::array<f32, I3dl2ReverbInfo::MaxDelayLines> MinDelayLineTimes{
5.0f,
6.0f,
13.0f,
14.0f,
};
constexpr std::array<f32, I3dl2ReverbInfo::MaxDelayLines> MaxDelayLineTimes{
45.7042007446f,
82.7817001343f,
149.938293457f,
271.575805664f,
};
constexpr std::array<f32, I3dl2ReverbInfo::MaxDelayLines> Decay0MaxDelayLineTimes{17.0f, 13.0f,
9.0f, 7.0f};
constexpr std::array<f32, I3dl2ReverbInfo::MaxDelayLines> Decay1MaxDelayLineTimes{19.0f, 11.0f,
10.0f, 6.0f};
constexpr std::array<f32, I3dl2ReverbInfo::MaxDelayTaps> EarlyTapTimes{
0.0171360000968f,
0.0591540001333f,
0.161733001471f,
0.390186011791f,
0.425262004137f,
0.455410987139f,
0.689737021923f,
0.74590998888f,
0.833844006062f,
0.859502017498f,
0.0f,
0.0750240013003f,
0.168788000941f,
0.299901008606f,
0.337442994118f,
0.371903002262f,
0.599011003971f,
0.716741025448f,
0.817858994007f,
0.85166400671f,
};
constexpr std::array<f32, I3dl2ReverbInfo::MaxDelayTaps> EarlyGains{
0.67096f, 0.61027f, 1.0f, 0.3568f, 0.68361f, 0.65978f, 0.51939f,
0.24712f, 0.45945f, 0.45021f, 0.64196f, 0.54879f, 0.92925f, 0.3827f,
0.72867f, 0.69794f, 0.5464f, 0.24563f, 0.45214f, 0.44042f};
/**
* Update the I3dl2ReverbInfo state according to the given parameters.
*
* @param params - Input parameters to update the state.
* @param state - State to be updated.
* @param reset - If enabled, the state buffers will be reset. Only set this on initialize.
*/
static void UpdateI3dl2ReverbEffectParameter(const I3dl2ReverbInfo::ParameterVersion1& params,
I3dl2ReverbInfo::State& state, const bool reset) {
const auto pow_10 = [](f32 val) -> f32 {
return (val >= 0.0f) ? 1.0f : (val <= -5.3f) ? 0.0f : std::pow(10.0f, val);
};
const auto sin = [](f32 degrees) -> f32 {
return std::sin(degrees * std::numbers::pi_v<f32> / 180.0f);
};
const auto cos = [](f32 degrees) -> f32 {
return std::cos(degrees * std::numbers::pi_v<f32> / 180.0f);
};
Common::FixedPoint<50, 14> delay{static_cast<f32>(params.sample_rate) / 1000.0f};
state.dry_gain = params.dry_gain;
Common::FixedPoint<50, 14> early_gain{
std::min(params.room_gain + params.reflection_gain, 5000.0f) / 2000.0f};
state.early_gain = pow_10(early_gain.to_float());
Common::FixedPoint<50, 14> late_gain{std::min(params.room_gain + params.reverb_gain, 5000.0f) /
2000.0f};
state.late_gain = pow_10(late_gain.to_float());
Common::FixedPoint<50, 14> hf_gain{pow_10(params.room_HF_gain / 2000.0f)};
if (hf_gain >= 1.0f) {
state.lowpass_1 = 0.0f;
state.lowpass_2 = 1.0f;
} else {
const auto reference_hf{(params.reference_HF * 256.0f) /
static_cast<f32>(params.sample_rate)};
const Common::FixedPoint<50, 14> a{1.0f - hf_gain.to_float()};
const Common::FixedPoint<50, 14> b{2.0f + (-cos(reference_hf) * (hf_gain * 2.0f))};
const Common::FixedPoint<50, 14> c{
std::sqrt(std::pow(b.to_float(), 2.0f) + (std::pow(a.to_float(), 2.0f) * -4.0f))};
state.lowpass_1 = std::min(((b - c) / (a * 2.0f)).to_float(), 0.99723f);
state.lowpass_2 = 1.0f - state.lowpass_1;
}
state.early_to_late_taps =
(((params.reflection_delay + params.late_reverb_delay_time) * 1000.0f) * delay).to_int();
state.last_reverb_echo = params.late_reverb_diffusion * 0.6f * 0.01f;
for (u32 i = 0; i < I3dl2ReverbInfo::MaxDelayLines; i++) {
auto curr_delay{
((MinDelayLineTimes[i] + (params.late_reverb_density / 100.0f) *
(MaxDelayLineTimes[i] - MinDelayLineTimes[i])) *
delay)
.to_int()};
state.fdn_delay_lines[i].SetDelay(curr_delay);
const auto a{
(static_cast<f32>(state.fdn_delay_lines[i].delay + state.decay_delay_lines0[i].delay +
state.decay_delay_lines1[i].delay) *
-60.0f) /
(params.late_reverb_decay_time * static_cast<f32>(params.sample_rate))};
const auto b{a / params.late_reverb_HF_decay_ratio};
const auto c{
cos(((params.reference_HF * 0.5f) * 128.0f) / static_cast<f32>(params.sample_rate)) /
sin(((params.reference_HF * 0.5f) * 128.0f) / static_cast<f32>(params.sample_rate))};
const auto d{pow_10((b - a) / 40.0f)};
const auto e{pow_10((b + a) / 40.0f) * 0.7071f};
state.lowpass_coeff[i][0] = ((c * d + 1.0f) * e) / (c + d);
state.lowpass_coeff[i][1] = ((1.0f - (c * d)) * e) / (c + d);
state.lowpass_coeff[i][2] = (c - d) / (c + d);
state.decay_delay_lines0[i].wet_gain = state.last_reverb_echo;
state.decay_delay_lines1[i].wet_gain = state.last_reverb_echo * -0.9f;
}
if (reset) {
state.shelf_filter.fill(0.0f);
state.lowpass_0 = 0.0f;
for (u32 i = 0; i < I3dl2ReverbInfo::MaxDelayLines; i++) {
std::ranges::fill(state.fdn_delay_lines[i].buffer, 0);
std::ranges::fill(state.decay_delay_lines0[i].buffer, 0);
std::ranges::fill(state.decay_delay_lines1[i].buffer, 0);
}
std::ranges::fill(state.center_delay_line.buffer, 0);
std::ranges::fill(state.early_delay_line.buffer, 0);
}
const auto reflection_time{(params.late_reverb_delay_time * 0.9998f + 0.02f) * 1000.0f};
const auto reflection_delay{params.reflection_delay * 1000.0f};
for (u32 i = 0; i < I3dl2ReverbInfo::MaxDelayTaps; i++) {
auto length{((reflection_delay + reflection_time * EarlyTapTimes[i]) * delay).to_int()};
if (length >= state.early_delay_line.max_delay) {
length = state.early_delay_line.max_delay;
}
state.early_tap_steps[i] = length;
}
}
/**
* Initialize a new I3dl2ReverbInfo state according to the given parameters.
*
* @param params - Input parameters to update the state.
* @param state - State to be updated.
* @param workbuffer - Game-supplied memory for the state. (Unused)
*/
static void InitializeI3dl2ReverbEffect(const I3dl2ReverbInfo::ParameterVersion1& params,
I3dl2ReverbInfo::State& state, const CpuAddr workbuffer) {
state = {};
Common::FixedPoint<50, 14> delay{static_cast<f32>(params.sample_rate) / 1000};
for (u32 i = 0; i < I3dl2ReverbInfo::MaxDelayLines; i++) {
auto fdn_delay_time{(MaxDelayLineTimes[i] * delay).to_uint_floor()};
state.fdn_delay_lines[i].Initialize(fdn_delay_time);
auto decay0_delay_time{(Decay0MaxDelayLineTimes[i] * delay).to_uint_floor()};
state.decay_delay_lines0[i].Initialize(decay0_delay_time);
auto decay1_delay_time{(Decay1MaxDelayLineTimes[i] * delay).to_uint_floor()};
state.decay_delay_lines1[i].Initialize(decay1_delay_time);
}
const auto center_delay_time{(5 * delay).to_uint_floor()};
state.center_delay_line.Initialize(center_delay_time);
const auto early_delay_time{(400 * delay).to_uint_floor()};
state.early_delay_line.Initialize(early_delay_time);
UpdateI3dl2ReverbEffectParameter(params, state, true);
}
/**
* Pass-through the effect, copying input to output directly, with no reverb applied.
*
* @param inputs - Array of input mix buffers to copy.
* @param outputs - Array of output mix buffers to receive copy.
* @param channel_count - Number of channels in inputs and outputs.
* @param sample_count - Number of samples within each channel (unused).
*/
static void ApplyI3dl2ReverbEffectBypass(std::span<std::span<const s32>> inputs,
std::span<std::span<s32>> outputs, const u32 channel_count,
[[maybe_unused]] const u32 sample_count) {
for (u32 i = 0; i < channel_count; i++) {
if (inputs[i].data() != outputs[i].data()) {
std::memcpy(outputs[i].data(), inputs[i].data(), outputs[i].size_bytes());
}
}
}
/**
* Tick the delay lines, reading and returning their current output, and writing a new decaying
* sample (mix).
*
* @param decay0 - The first decay line.
* @param decay1 - The second decay line.
* @param fdn - Feedback delay network.
* @param mix - The new calculated sample to be written and decayed.
* @return The next delayed and decayed sample.
*/
static Common::FixedPoint<50, 14> Axfx2AllPassTick(I3dl2ReverbInfo::I3dl2DelayLine& decay0,
I3dl2ReverbInfo::I3dl2DelayLine& decay1,
I3dl2ReverbInfo::I3dl2DelayLine& fdn,
const Common::FixedPoint<50, 14> mix) {
auto val{decay0.Read()};
auto mixed{mix - (val * decay0.wet_gain)};
auto out{decay0.Tick(mixed) + (mixed * decay0.wet_gain)};
val = decay1.Read();
mixed = out - (val * decay1.wet_gain);
out = decay1.Tick(mixed) + (mixed * decay1.wet_gain);
fdn.Tick(out);
return out;
}
/**
* Impl. Apply a I3DL2 reverb according to the current state, on the input mix buffers,
* saving the results to the output mix buffers.
*
* @tparam NumChannels - Number of channels to process. 1-6.
Inputs/outputs should have this many buffers.
* @param state - State to use, must be initialized (see InitializeI3dl2ReverbEffect).
* @param inputs - Input mix buffers to perform the reverb on.
* @param outputs - Output mix buffers to receive the reverbed samples.
* @param sample_count - Number of samples to process.
*/
template <size_t NumChannels>
static void ApplyI3dl2ReverbEffect(I3dl2ReverbInfo::State& state,
std::span<std::span<const s32>> inputs,
std::span<std::span<s32>> outputs, const u32 sample_count) {
static constexpr std::array<u8, I3dl2ReverbInfo::MaxDelayTaps> OutTapIndexes1Ch{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
};
static constexpr std::array<u8, I3dl2ReverbInfo::MaxDelayTaps> OutTapIndexes2Ch{
0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1,
};
static constexpr std::array<u8, I3dl2ReverbInfo::MaxDelayTaps> OutTapIndexes4Ch{
0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 0, 0, 0, 0, 3, 3, 3,
};
static constexpr std::array<u8, I3dl2ReverbInfo::MaxDelayTaps> OutTapIndexes6Ch{
2, 0, 0, 1, 1, 1, 1, 4, 4, 4, 1, 1, 1, 0, 0, 0, 0, 5, 5, 5,
};
std::span<const u8> tap_indexes{};
if constexpr (NumChannels == 1) {
tap_indexes = OutTapIndexes1Ch;
} else if constexpr (NumChannels == 2) {
tap_indexes = OutTapIndexes2Ch;
} else if constexpr (NumChannels == 4) {
tap_indexes = OutTapIndexes4Ch;
} else if constexpr (NumChannels == 6) {
tap_indexes = OutTapIndexes6Ch;
}
for (u32 sample_index = 0; sample_index < sample_count; sample_index++) {
Common::FixedPoint<50, 14> early_to_late_tap{
state.early_delay_line.TapOut(state.early_to_late_taps)};
std::array<Common::FixedPoint<50, 14>, NumChannels> output_samples{};
for (u32 early_tap = 0; early_tap < I3dl2ReverbInfo::MaxDelayTaps; early_tap++) {
output_samples[tap_indexes[early_tap]] +=
state.early_delay_line.TapOut(state.early_tap_steps[early_tap]) *
EarlyGains[early_tap];
if constexpr (NumChannels == 6) {
output_samples[static_cast<u32>(Channels::LFE)] +=
state.early_delay_line.TapOut(state.early_tap_steps[early_tap]) *
EarlyGains[early_tap];
}
}
Common::FixedPoint<50, 14> current_sample{};
for (u32 channel = 0; channel < NumChannels; channel++) {
current_sample += inputs[channel][sample_index];
}
state.lowpass_0 =
(current_sample * state.lowpass_2 + state.lowpass_0 * state.lowpass_1).to_float();
state.early_delay_line.Tick(state.lowpass_0);
for (u32 channel = 0; channel < NumChannels; channel++) {
output_samples[channel] *= state.early_gain;
}
std::array<Common::FixedPoint<50, 14>, I3dl2ReverbInfo::MaxDelayLines> filtered_samples{};
for (u32 delay_line = 0; delay_line < I3dl2ReverbInfo::MaxDelayLines; delay_line++) {
filtered_samples[delay_line] =
state.fdn_delay_lines[delay_line].Read() * state.lowpass_coeff[delay_line][0] +
state.shelf_filter[delay_line];
state.shelf_filter[delay_line] =
(filtered_samples[delay_line] * state.lowpass_coeff[delay_line][2] +
state.fdn_delay_lines[delay_line].Read() * state.lowpass_coeff[delay_line][1])
.to_float();
}
const std::array<Common::FixedPoint<50, 14>, I3dl2ReverbInfo::MaxDelayLines> mix_matrix{
filtered_samples[1] + filtered_samples[2] + early_to_late_tap * state.late_gain,
-filtered_samples[0] - filtered_samples[3] + early_to_late_tap * state.late_gain,
filtered_samples[0] - filtered_samples[3] + early_to_late_tap * state.late_gain,
filtered_samples[1] - filtered_samples[2] + early_to_late_tap * state.late_gain,
};
std::array<Common::FixedPoint<50, 14>, I3dl2ReverbInfo::MaxDelayLines> allpass_samples{};
for (u32 delay_line = 0; delay_line < I3dl2ReverbInfo::MaxDelayLines; delay_line++) {
allpass_samples[delay_line] = Axfx2AllPassTick(
state.decay_delay_lines0[delay_line], state.decay_delay_lines1[delay_line],
state.fdn_delay_lines[delay_line], mix_matrix[delay_line]);
}
if constexpr (NumChannels == 6) {
const std::array<Common::FixedPoint<50, 14>, MaxChannels> allpass_outputs{
allpass_samples[0], allpass_samples[1], allpass_samples[2] - allpass_samples[3],
allpass_samples[3], allpass_samples[2], allpass_samples[3],
};
for (u32 channel = 0; channel < NumChannels; channel++) {
Common::FixedPoint<50, 14> allpass{};
if (channel == static_cast<u32>(Channels::Center)) {
allpass = state.center_delay_line.Tick(allpass_outputs[channel] * 0.5f);
} else {
allpass = allpass_outputs[channel];
}
auto out_sample{output_samples[channel] + allpass +
state.dry_gain * static_cast<f32>(inputs[channel][sample_index])};
outputs[channel][sample_index] =
static_cast<s32>(std::clamp(out_sample.to_float(), -8388600.0f, 8388600.0f));
}
} else {
for (u32 channel = 0; channel < NumChannels; channel++) {
auto out_sample{output_samples[channel] + allpass_samples[channel] +
state.dry_gain * static_cast<f32>(inputs[channel][sample_index])};
outputs[channel][sample_index] =
static_cast<s32>(std::clamp(out_sample.to_float(), -8388600.0f, 8388600.0f));
}
}
}
}
/**
* Apply a I3DL2 reverb if enabled, according to the current state, on the input mix buffers,
* saving the results to the output mix buffers.
*
* @param params - Input parameters to use.
* @param state - State to use, must be initialized (see InitializeI3dl2ReverbEffect).
* @param enabled - If enabled, delay will be applied, otherwise input is copied to output.
* @param inputs - Input mix buffers to performan the delay on.
* @param outputs - Output mix buffers to receive the delayed samples.
* @param sample_count - Number of samples to process.
*/
static void ApplyI3dl2ReverbEffect(const I3dl2ReverbInfo::ParameterVersion1& params,
I3dl2ReverbInfo::State& state, const bool enabled,
std::span<std::span<const s32>> inputs,
std::span<std::span<s32>> outputs, const u32 sample_count) {
if (enabled) {
switch (params.channel_count) {
case 0:
return;
case 1:
ApplyI3dl2ReverbEffect<1>(state, inputs, outputs, sample_count);
break;
case 2:
ApplyI3dl2ReverbEffect<2>(state, inputs, outputs, sample_count);
break;
case 4:
ApplyI3dl2ReverbEffect<4>(state, inputs, outputs, sample_count);
break;
case 6:
ApplyI3dl2ReverbEffect<6>(state, inputs, outputs, sample_count);
break;
default:
ApplyI3dl2ReverbEffectBypass(inputs, outputs, params.channel_count, sample_count);
break;
}
} else {
ApplyI3dl2ReverbEffectBypass(inputs, outputs, params.channel_count, sample_count);
}
}
void I3dl2ReverbCommand::Dump([[maybe_unused]] const AudioRenderer::CommandListProcessor& processor,
std::string& string) {
string += fmt::format("I3dl2ReverbCommand\n\tenabled {} \n\tinputs: ", effect_enabled);
for (u32 i = 0; i < parameter.channel_count; i++) {
string += fmt::format("{:02X}, ", inputs[i]);
}
string += "\n\toutputs: ";
for (u32 i = 0; i < parameter.channel_count; i++) {
string += fmt::format("{:02X}, ", outputs[i]);
}
string += "\n";
}
void I3dl2ReverbCommand::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_{reinterpret_cast<I3dl2ReverbInfo::State*>(state)};
if (effect_enabled) {
if (parameter.state == I3dl2ReverbInfo::ParameterState::Updating) {
UpdateI3dl2ReverbEffectParameter(parameter, *state_, false);
} else if (parameter.state == I3dl2ReverbInfo::ParameterState::Initialized) {
InitializeI3dl2ReverbEffect(parameter, *state_, workbuffer);
}
}
ApplyI3dl2ReverbEffect(parameter, *state_, effect_enabled, input_buffers, output_buffers,
processor.sample_count);
}
bool I3dl2ReverbCommand::Verify(const AudioRenderer::CommandListProcessor& processor) {
return true;
}
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu 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/i3dl2.h"
#include "common/common_types.h"
namespace AudioCore::ADSP::AudioRenderer {
class CommandListProcessor;
}
namespace AudioCore::Renderer {
/**
* AudioRenderer command for a I3DL2Reverb effect. Apply a reverb to inputs mix buffer according to
* the I3DL2 spec, outputs receives the results.
*/
struct I3dl2ReverbCommand : 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
I3dl2ReverbInfo::ParameterVersion1 parameter;
/// State, updated each call
CpuAddr state;
/// Game-supplied workbuffer (Unused)
CpuAddr workbuffer;
/// Is this effect enabled?
bool effect_enabled;
};
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,222 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include "audio_core/adsp/apps/audio_renderer/command_list_processor.h"
#include "audio_core/renderer/command/effect/light_limiter.h"
namespace AudioCore::Renderer {
/**
* Update the LightLimiterInfo state according to the given parameters.
* A no-op.
*
* @param params - Input parameters to update the state.
* @param state - State to be updated.
*/
static void UpdateLightLimiterEffectParameter(const LightLimiterInfo::ParameterVersion2& params,
LightLimiterInfo::State& state) {}
/**
* Initialize a new LightLimiterInfo state according to the given parameters.
*
* @param params - Input parameters to update the state.
* @param state - State to be updated.
* @param workbuffer - Game-supplied memory for the state. (Unused)
*/
static void InitializeLightLimiterEffect(const LightLimiterInfo::ParameterVersion2& params,
LightLimiterInfo::State& state, const CpuAddr workbuffer) {
state = {};
state.samples_average.fill(0.0f);
state.compression_gain.fill(1.0f);
state.look_ahead_sample_offsets.fill(0);
for (u32 i = 0; i < params.channel_count; i++) {
state.look_ahead_sample_buffers[i].resize(params.look_ahead_samples_max, 0.0f);
}
}
/**
* Apply a light limiter effect if enabled, according to the current state, on the input mix
* buffers, saving the results to the output mix buffers.
*
* @param params - Input parameters to use.
* @param state - State to use, must be initialized (see InitializeLightLimiterEffect).
* @param enabled - If enabled, limiter will be applied, otherwise input is copied to output.
* @param inputs - Input mix buffers to perform the limiter on.
* @param outputs - Output mix buffers to receive the limited samples.
* @param sample_count - Number of samples to process.
* @params statistics - Optional output statistics, only used with version 2.
*/
static void ApplyLightLimiterEffect(const LightLimiterInfo::ParameterVersion2& params,
LightLimiterInfo::State& state, const bool enabled,
std::span<std::span<const s32>> inputs,
std::span<std::span<s32>> outputs, const u32 sample_count,
LightLimiterInfo::StatisticsInternal* statistics) {
constexpr s64 min{std::numeric_limits<s32>::min()};
constexpr s64 max{std::numeric_limits<s32>::max()};
const auto recip_estimate = [](f64 a) -> f64 {
s32 q, s;
f64 r;
q = (s32)(a * 512.0); /* a in units of 1/512 rounded down */
r = 1.0 / (((f64)q + 0.5) / 512.0); /* reciprocal r */
s = (s32)(256.0 * r + 0.5); /* r in units of 1/256 rounded to nearest */
return ((f64)s / 256.0);
};
if (enabled) {
if (statistics && params.statistics_reset_required) {
for (u32 i = 0; i < params.channel_count; i++) {
statistics->channel_compression_gain_min[i] = 1.0f;
statistics->channel_max_sample[i] = 0;
}
}
for (u32 sample_index = 0; sample_index < sample_count; sample_index++) {
for (u32 channel = 0; channel < params.channel_count; channel++) {
auto sample{(Common::FixedPoint<49, 15>(inputs[channel][sample_index]) /
Common::FixedPoint<49, 15>::one) *
params.input_gain};
auto abs_sample{sample};
if (sample < 0.0f) {
abs_sample = -sample;
}
auto coeff{abs_sample > state.samples_average[channel] ? params.attack_coeff
: params.release_coeff};
state.samples_average[channel] +=
((abs_sample - state.samples_average[channel]) * coeff).to_float();
// Reciprocal estimate
auto new_average_sample{Common::FixedPoint<49, 15>(
recip_estimate(state.samples_average[channel].to_double()))};
if (params.processing_mode != LightLimiterInfo::ProcessingMode::Mode1) {
// Two Newton-Raphson steps
auto temp{2.0 - (state.samples_average[channel] * new_average_sample)};
new_average_sample = 2.0 - (state.samples_average[channel] * temp);
}
auto above_threshold{state.samples_average[channel] > params.threshold};
auto attenuation{above_threshold ? params.threshold * new_average_sample : 1.0f};
coeff = attenuation < state.compression_gain[channel] ? params.attack_coeff
: params.release_coeff;
state.compression_gain[channel] +=
(attenuation - state.compression_gain[channel]) * coeff;
auto lookahead_sample{
state.look_ahead_sample_buffers[channel]
[state.look_ahead_sample_offsets[channel]]};
state.look_ahead_sample_buffers[channel][state.look_ahead_sample_offsets[channel]] =
sample;
state.look_ahead_sample_offsets[channel] =
(state.look_ahead_sample_offsets[channel] + 1) % params.look_ahead_samples_min;
outputs[channel][sample_index] = static_cast<s32>(
std::clamp((lookahead_sample * state.compression_gain[channel] *
params.output_gain * Common::FixedPoint<49, 15>::one)
.to_long(),
min, max));
if (statistics) {
statistics->channel_max_sample[channel] =
std::max(statistics->channel_max_sample[channel], abs_sample.to_float());
statistics->channel_compression_gain_min[channel] =
std::min(statistics->channel_compression_gain_min[channel],
state.compression_gain[channel].to_float());
}
}
}
} else {
for (u32 i = 0; i < params.channel_count; i++) {
if (params.inputs[i] != params.outputs[i]) {
std::memcpy(outputs[i].data(), inputs[i].data(), outputs[i].size_bytes());
}
}
}
}
void LightLimiterVersion1Command::Dump(
[[maybe_unused]] const AudioRenderer::CommandListProcessor& processor, std::string& string) {
string += fmt::format("LightLimiterVersion1Command\n\tinputs: ");
for (u32 i = 0; i < MaxChannels; i++) {
string += fmt::format("{:02X}, ", inputs[i]);
}
string += "\n\toutputs: ";
for (u32 i = 0; i < MaxChannels; i++) {
string += fmt::format("{:02X}, ", outputs[i]);
}
string += "\n";
}
void LightLimiterVersion1Command::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_{reinterpret_cast<LightLimiterInfo::State*>(state)};
if (effect_enabled) {
if (parameter.state == LightLimiterInfo::ParameterState::Updating) {
UpdateLightLimiterEffectParameter(parameter, *state_);
} else if (parameter.state == LightLimiterInfo::ParameterState::Initialized) {
InitializeLightLimiterEffect(parameter, *state_, workbuffer);
}
}
LightLimiterInfo::StatisticsInternal* statistics{nullptr};
ApplyLightLimiterEffect(parameter, *state_, effect_enabled, input_buffers, output_buffers,
processor.sample_count, statistics);
}
bool LightLimiterVersion1Command::Verify(const AudioRenderer::CommandListProcessor& processor) {
return true;
}
void LightLimiterVersion2Command::Dump(
[[maybe_unused]] const AudioRenderer::CommandListProcessor& processor, std::string& string) {
string += fmt::format("LightLimiterVersion2Command\n\tinputs: \n");
for (u32 i = 0; i < MaxChannels; i++) {
string += fmt::format("{:02X}, ", inputs[i]);
}
string += "\n\toutputs: ";
for (u32 i = 0; i < MaxChannels; i++) {
string += fmt::format("{:02X}, ", outputs[i]);
}
string += "\n";
}
void LightLimiterVersion2Command::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_{reinterpret_cast<LightLimiterInfo::State*>(state)};
if (effect_enabled) {
if (parameter.state == LightLimiterInfo::ParameterState::Updating) {
UpdateLightLimiterEffectParameter(parameter, *state_);
} else if (parameter.state == LightLimiterInfo::ParameterState::Initialized) {
InitializeLightLimiterEffect(parameter, *state_, workbuffer);
}
}
auto statistics{reinterpret_cast<LightLimiterInfo::StatisticsInternal*>(result_state)};
ApplyLightLimiterEffect(parameter, *state_, effect_enabled, input_buffers, output_buffers,
processor.sample_count, statistics);
}
bool LightLimiterVersion2Command::Verify(const AudioRenderer::CommandListProcessor& processor) {
return true;
}
} // namespace AudioCore::Renderer

View File

@@ -0,0 +1,104 @@
// SPDX-FileCopyrightText: Copyright 2022 yuzu 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/light_limiter.h"
#include "common/common_types.h"
namespace AudioCore::ADSP::AudioRenderer {
class CommandListProcessor;
}
namespace AudioCore::Renderer {
/**
* AudioRenderer command for limiting volume between a high and low threshold.
* Version 1.
*/
struct LightLimiterVersion1Command : 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
LightLimiterInfo::ParameterVersion2 parameter;
/// State, updated each call
CpuAddr state;
/// Game-supplied workbuffer (Unused)
CpuAddr workbuffer;
/// Is this effect enabled?
bool effect_enabled;
};
/**
* AudioRenderer command for limiting volume between a high and low threshold.
* Version 2 with output statistics.
*/
struct LightLimiterVersion2Command : 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.
*/
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
LightLimiterInfo::ParameterVersion2 parameter;
/// State, updated each call
CpuAddr state;
/// Game-supplied workbuffer (Unused)
CpuAddr workbuffer;
/// Optional statistics, sent back to the sysmodule
CpuAddr result_state;
/// Is this effect enabled?
bool effect_enabled;
};
} // namespace AudioCore::Renderer

Some files were not shown because too many files have changed in this diff Show More