From 458da8a94877677f086f06cdeecf959ec4283a33 Mon Sep 17 00:00:00 2001
From: Kelebek1 <eeeedddccc@hotmail.co.uk>
Date: Sat, 16 Jul 2022 23:48:45 +0100
Subject: [PATCH] Project Andio

---
 .gitmodules                                   |    2 +-
 src/audio_core/CMakeLists.txt                 |  253 +-
 src/audio_core/algorithm/filter.cpp           |   79 -
 src/audio_core/algorithm/filter.h             |   61 -
 src/audio_core/algorithm/interpolate.cpp      |  232 --
 src/audio_core/algorithm/interpolate.h        |   43 -
 src/audio_core/audio_core.cpp                 |   68 +
 src/audio_core/audio_core.h                   |  100 +
 src/audio_core/audio_event.cpp                |   61 +
 src/audio_core/audio_event.h                  |   92 +
 src/audio_core/audio_in_manager.cpp           |   91 +
 src/audio_core/audio_in_manager.h             |   92 +
 src/audio_core/audio_manager.cpp              |   80 +
 src/audio_core/audio_manager.h                |  101 +
 src/audio_core/audio_out.cpp                  |   62 -
 src/audio_core/audio_out.h                    |   49 -
 src/audio_core/audio_out_manager.cpp          |   81 +
 src/audio_core/audio_out_manager.h            |   89 +
 src/audio_core/audio_render_manager.cpp       |   70 +
 src/audio_core/audio_render_manager.h         |  103 +
 src/audio_core/audio_renderer.cpp             |  343 --
 src/audio_core/audio_renderer.h               |   78 -
 src/audio_core/behavior_info.cpp              |  104 -
 src/audio_core/behavior_info.h                |   71 -
 src/audio_core/buffer.h                       |   44 -
 src/audio_core/codec.cpp                      |   77 -
 src/audio_core/codec.h                        |   43 -
 src/audio_core/command_generator.cpp          | 1369 -------
 src/audio_core/command_generator.h            |  110 -
 src/audio_core/common.h                       |  132 -
 .../common/audio_renderer_parameter.h         |   60 +
 src/audio_core/common/common.h                |  138 +
 src/audio_core/common/feature_support.h       |  105 +
 src/audio_core/common/wave_buffer.h           |   35 +
 src/audio_core/common/workbuffer_allocator.h  |  100 +
 src/audio_core/cubeb_sink.cpp                 |  249 --
 src/audio_core/cubeb_sink.h                   |   35 -
 src/audio_core/delay_line.cpp                 |  107 -
 src/audio_core/delay_line.h                   |   49 -
 src/audio_core/device/audio_buffer.h          |   21 +
 src/audio_core/device/audio_buffers.h         |  304 ++
 src/audio_core/device/device_session.cpp      |  114 +
 src/audio_core/device/device_session.h        |  126 +
 src/audio_core/effect_context.cpp             |  320 --
 src/audio_core/effect_context.h               |  349 --
 src/audio_core/in/audio_in.cpp                |  100 +
 src/audio_core/in/audio_in.h                  |  147 +
 src/audio_core/in/audio_in_system.cpp         |  213 +
 src/audio_core/in/audio_in_system.h           |  275 ++
 src/audio_core/info_updater.cpp               |  511 ---
 src/audio_core/info_updater.h                 |   57 -
 src/audio_core/memory_pool.cpp                |   60 -
 src/audio_core/memory_pool.h                  |   51 -
 src/audio_core/mix_context.cpp                |  297 --
 src/audio_core/mix_context.h                  |  113 -
 src/audio_core/null_sink.h                    |   32 -
 src/audio_core/out/audio_out.cpp              |  100 +
 src/audio_core/out/audio_out.h                |  147 +
 src/audio_core/out/audio_out_system.cpp       |  207 +
 src/audio_core/out/audio_out_system.h         |  257 ++
 src/audio_core/renderer/adsp/adsp.cpp         |  118 +
 src/audio_core/renderer/adsp/adsp.h           |  173 +
 .../renderer/adsp/audio_renderer.cpp          |  226 +
 src/audio_core/renderer/adsp/audio_renderer.h |  203 +
 src/audio_core/renderer/adsp/command_buffer.h |   21 +
 .../renderer/adsp/command_list_processor.cpp  |  109 +
 .../renderer/adsp/command_list_processor.h    |  118 +
 src/audio_core/renderer/audio_device.cpp      |   52 +
 src/audio_core/renderer/audio_device.h        |   88 +
 src/audio_core/renderer/audio_renderer.cpp    |   67 +
 src/audio_core/renderer/audio_renderer.h      |   97 +
 .../renderer/behavior/behavior_info.cpp       |  191 +
 .../renderer/behavior/behavior_info.h         |  376 ++
 .../renderer/behavior/info_updater.cpp        |  539 +++
 .../renderer/behavior/info_updater.h          |  205 +
 .../renderer/command/command_buffer.cpp       |  714 ++++
 .../renderer/command/command_buffer.h         |  466 +++
 .../renderer/command/command_generator.cpp    |  796 ++++
 .../renderer/command/command_generator.h      |  349 ++
 .../renderer/command/command_list_header.h    |   22 +
 .../command_processing_time_estimator.cpp     | 3620 +++++++++++++++++
 .../command_processing_time_estimator.h       |  254 ++
 src/audio_core/renderer/command/commands.h    |   32 +
 .../renderer/command/data_source/adpcm.cpp    |   84 +
 .../renderer/command/data_source/adpcm.h      |  119 +
 .../renderer/command/data_source/decode.cpp   |  428 ++
 .../renderer/command/data_source/decode.h     |   59 +
 .../command/data_source/pcm_float.cpp         |   86 +
 .../renderer/command/data_source/pcm_float.h  |  113 +
 .../command/data_source/pcm_int16.cpp         |   87 +
 .../renderer/command/data_source/pcm_int16.h  |  110 +
 .../renderer/command/effect/aux_.cpp          |  207 +
 src/audio_core/renderer/command/effect/aux_.h |   66 +
 .../renderer/command/effect/biquad_filter.cpp |  118 +
 .../renderer/command/effect/biquad_filter.h   |   74 +
 .../renderer/command/effect/capture.cpp       |  142 +
 .../renderer/command/effect/capture.h         |   62 +
 .../renderer/command/effect/compressor.cpp    |  156 +
 .../renderer/command/effect/compressor.h      |   60 +
 .../renderer/command/effect/delay.cpp         |  238 ++
 .../renderer/command/effect/delay.h           |   60 +
 .../renderer/command/effect/i3dl2_reverb.cpp  |  437 ++
 .../renderer/command/effect/i3dl2_reverb.h    |   60 +
 .../renderer/command/effect/light_limiter.cpp |  222 +
 .../renderer/command/effect/light_limiter.h   |  103 +
 .../effect/multi_tap_biquad_filter.cpp        |   45 +
 .../command/effect/multi_tap_biquad_filter.h  |   59 +
 .../renderer/command/effect/reverb.cpp        |  440 ++
 .../renderer/command/effect/reverb.h          |   62 +
 src/audio_core/renderer/command/icommand.h    |   93 +
 .../renderer/command/mix/clear_mix.cpp        |   24 +
 .../renderer/command/mix/clear_mix.h          |   45 +
 .../renderer/command/mix/copy_mix.cpp         |   27 +
 .../renderer/command/mix/copy_mix.h           |   49 +
 .../command/mix/depop_for_mix_buffers.cpp     |   64 +
 .../command/mix/depop_for_mix_buffers.h       |   55 +
 .../renderer/command/mix/depop_prepare.cpp    |   36 +
 .../renderer/command/mix/depop_prepare.h      |   54 +
 src/audio_core/renderer/command/mix/mix.cpp   |   70 +
 src/audio_core/renderer/command/mix/mix.h     |   54 +
 .../renderer/command/mix/mix_ramp.cpp         |   94 +
 .../renderer/command/mix/mix_ramp.h           |   73 +
 .../renderer/command/mix/mix_ramp_grouped.cpp |   65 +
 .../renderer/command/mix/mix_ramp_grouped.h   |   61 +
 .../renderer/command/mix/volume.cpp           |   72 +
 src/audio_core/renderer/command/mix/volume.h  |   53 +
 .../renderer/command/mix/volume_ramp.cpp      |   84 +
 .../renderer/command/mix/volume_ramp.h        |   56 +
 .../command/performance/performance.cpp       |   43 +
 .../command/performance/performance.h         |   51 +
 .../command/resample/downmix_6ch_to_2ch.cpp   |   74 +
 .../command/resample/downmix_6ch_to_2ch.h     |   59 +
 .../renderer/command/resample/resample.cpp    |  883 ++++
 .../renderer/command/resample/resample.h      |   29 +
 .../renderer/command/resample/upsample.cpp    |  262 ++
 .../renderer/command/resample/upsample.h      |   60 +
 .../renderer/command/sink/circular_buffer.cpp |   48 +
 .../renderer/command/sink/circular_buffer.h   |   55 +
 .../renderer/command/sink/device.cpp          |   55 +
 src/audio_core/renderer/command/sink/device.h |   57 +
 src/audio_core/renderer/effect/aux_.cpp       |   93 +
 src/audio_core/renderer/effect/aux_.h         |  123 +
 .../renderer/effect/biquad_filter.cpp         |   52 +
 .../renderer/effect/biquad_filter.h           |   79 +
 .../renderer/effect/buffer_mixer.cpp          |   49 +
 src/audio_core/renderer/effect/buffer_mixer.h |   75 +
 src/audio_core/renderer/effect/capture.cpp    |   82 +
 src/audio_core/renderer/effect/capture.h      |   65 +
 src/audio_core/renderer/effect/compressor.cpp |   40 +
 src/audio_core/renderer/effect/compressor.h   |  106 +
 src/audio_core/renderer/effect/delay.cpp      |   93 +
 src/audio_core/renderer/effect/delay.h        |  135 +
 .../renderer/effect/effect_context.cpp        |   41 +
 .../renderer/effect/effect_context.h          |   75 +
 .../renderer/effect/effect_info_base.h        |  435 ++
 src/audio_core/renderer/effect/effect_reset.h |   71 +
 .../renderer/effect/effect_result_state.h     |   16 +
 src/audio_core/renderer/effect/i3dl2.cpp      |   94 +
 src/audio_core/renderer/effect/i3dl2.h        |  200 +
 .../renderer/effect/light_limiter.cpp         |   81 +
 .../renderer/effect/light_limiter.h           |  138 +
 src/audio_core/renderer/effect/reverb.cpp     |   93 +
 src/audio_core/renderer/effect/reverb.h       |  190 +
 src/audio_core/renderer/memory/address_info.h |  125 +
 .../renderer/memory/memory_pool_info.cpp      |   61 +
 .../renderer/memory/memory_pool_info.h        |  170 +
 .../renderer/memory/pool_mapper.cpp           |  243 ++
 src/audio_core/renderer/memory/pool_mapper.h  |  179 +
 src/audio_core/renderer/mix/mix_context.cpp   |  141 +
 src/audio_core/renderer/mix/mix_context.h     |  124 +
 src/audio_core/renderer/mix/mix_info.cpp      |  120 +
 src/audio_core/renderer/mix/mix_info.h        |  124 +
 src/audio_core/renderer/nodes/bit_array.h     |   25 +
 src/audio_core/renderer/nodes/edge_matrix.cpp |   38 +
 src/audio_core/renderer/nodes/edge_matrix.h   |   82 +
 src/audio_core/renderer/nodes/node_states.cpp |  141 +
 src/audio_core/renderer/nodes/node_states.h   |  195 +
 .../renderer/performance/detail_aspect.cpp    |   25 +
 .../renderer/performance/detail_aspect.h      |   33 +
 .../renderer/performance/entry_aspect.cpp     |   23 +
 .../renderer/performance/entry_aspect.h       |   32 +
 .../renderer/performance/performance_detail.h |   50 +
 .../renderer/performance/performance_entry.h  |   37 +
 .../performance/performance_entry_addresses.h |   17 +
 .../performance/performance_frame_header.h    |   36 +
 .../performance/performance_manager.cpp       |  645 +++
 .../performance/performance_manager.h         |  273 ++
 .../sink/circular_buffer_sink_info.cpp        |   76 +
 .../renderer/sink/circular_buffer_sink_info.h |   41 +
 .../renderer/sink/device_sink_info.cpp        |   57 +
 .../renderer/sink/device_sink_info.h          |   40 +
 src/audio_core/renderer/sink/sink_context.cpp |   21 +
 src/audio_core/renderer/sink/sink_context.h   |   47 +
 .../renderer/sink/sink_info_base.cpp          |   51 +
 src/audio_core/renderer/sink/sink_info_base.h |  177 +
 .../renderer/splitter/splitter_context.cpp    |  217 +
 .../renderer/splitter/splitter_context.h      |  189 +
 .../splitter/splitter_destinations_data.cpp   |   87 +
 .../splitter/splitter_destinations_data.h     |  135 +
 .../renderer/splitter/splitter_info.cpp       |   79 +
 .../renderer/splitter/splitter_info.h         |  107 +
 src/audio_core/renderer/system.cpp            |  802 ++++
 src/audio_core/renderer/system.h              |  307 ++
 src/audio_core/renderer/system_manager.cpp    |  162 +
 src/audio_core/renderer/system_manager.h      |  113 +
 .../renderer/upsampler/upsampler_info.cpp     |    6 +
 .../renderer/upsampler/upsampler_info.h       |   35 +
 .../renderer/upsampler/upsampler_manager.cpp  |   44 +
 .../renderer/upsampler/upsampler_manager.h    |   45 +
 .../renderer/upsampler/upsampler_state.h      |   40 +
 .../renderer/voice/voice_channel_resource.h   |   38 +
 .../renderer/voice/voice_context.cpp          |   86 +
 src/audio_core/renderer/voice/voice_context.h |  126 +
 src/audio_core/renderer/voice/voice_info.cpp  |  408 ++
 src/audio_core/renderer/voice/voice_info.h    |  378 ++
 src/audio_core/renderer/voice/voice_state.h   |   70 +
 src/audio_core/sdl2_sink.cpp                  |  160 -
 src/audio_core/sdl2_sink.h                    |   28 -
 src/audio_core/sink.h                         |   30 -
 src/audio_core/sink/cubeb_sink.cpp            |  651 +++
 src/audio_core/sink/cubeb_sink.h              |  110 +
 src/audio_core/sink/null_sink.h               |   52 +
 src/audio_core/sink/sdl2_sink.cpp             |  556 +++
 src/audio_core/sink/sdl2_sink.h               |  101 +
 src/audio_core/sink/sink.h                    |  106 +
 src/audio_core/{ => sink}/sink_details.cpp    |   27 +-
 src/audio_core/sink/sink_details.h            |   43 +
 src/audio_core/sink/sink_stream.h             |  224 +
 src/audio_core/sink_context.cpp               |   47 -
 src/audio_core/sink_context.h                 |   95 -
 src/audio_core/sink_details.h                 |   23 -
 src/audio_core/sink_stream.h                  |   35 -
 src/audio_core/splitter_context.cpp           |  616 ---
 src/audio_core/splitter_context.h             |  218 -
 src/audio_core/stream.cpp                     |  175 -
 src/audio_core/stream.h                       |  130 -
 src/audio_core/voice_context.cpp              |  579 ---
 src/audio_core/voice_context.h                |  302 --
 src/common/CMakeLists.txt                     |    3 +
 src/common/atomic_helpers.h                   |  772 ++++
 src/common/fixed_point.h                      |  726 ++++
 src/common/reader_writer_queue.h              |  941 +++++
 src/common/settings.cpp                       |    3 +-
 src/common/settings.h                         |    4 +-
 src/core/core.cpp                             |   51 +-
 src/core/core.h                               |   19 +
 src/core/hle/kernel/hle_ipc.cpp               |   38 +-
 src/core/hle/kernel/hle_ipc.h                 |    8 +
 src/core/hle/kernel/kernel.cpp                |   34 +-
 src/core/hle/kernel/kernel.h                  |    3 +
 src/core/hle/service/am/am.cpp                |   10 +-
 src/core/hle/service/am/am.h                  |    1 +
 src/core/hle/service/audio/audin_u.cpp        |  390 +-
 src/core/hle/service/audio/audin_u.h          |   47 +-
 src/core/hle/service/audio/audout_u.cpp       |  333 +-
 src/core/hle/service/audio/audout_u.h         |   21 +-
 src/core/hle/service/audio/audren_u.cpp       |  696 ++--
 src/core/hle/service/audio/audren_u.h         |   22 +-
 src/core/hle/service/audio/errors.h           |    9 +
 src/core/hle/service/audio/hwopus.cpp         |    2 +-
 src/core/memory.cpp                           |    6 +-
 src/core/memory.h                             |   13 +
 src/yuzu/configuration/config.cpp             |    6 +-
 src/yuzu/configuration/configure_audio.cpp    |   95 +-
 src/yuzu/configuration/configure_audio.h      |    4 +-
 src/yuzu/configuration/configure_audio.ui     |   28 +-
 src/yuzu/configuration/configure_debug.cpp    |    2 +
 src/yuzu/configuration/configure_debug.ui     |   11 +
 src/yuzu/main.cpp                             |    3 +
 src/yuzu_cmd/config.cpp                       |    2 +-
 270 files changed, 33712 insertions(+), 8445 deletions(-)
 delete mode 100644 src/audio_core/algorithm/filter.cpp
 delete mode 100644 src/audio_core/algorithm/filter.h
 delete mode 100644 src/audio_core/algorithm/interpolate.cpp
 delete mode 100644 src/audio_core/algorithm/interpolate.h
 create mode 100644 src/audio_core/audio_core.cpp
 create mode 100644 src/audio_core/audio_core.h
 create mode 100644 src/audio_core/audio_event.cpp
 create mode 100644 src/audio_core/audio_event.h
 create mode 100644 src/audio_core/audio_in_manager.cpp
 create mode 100644 src/audio_core/audio_in_manager.h
 create mode 100644 src/audio_core/audio_manager.cpp
 create mode 100644 src/audio_core/audio_manager.h
 delete mode 100644 src/audio_core/audio_out.cpp
 delete mode 100644 src/audio_core/audio_out.h
 create mode 100644 src/audio_core/audio_out_manager.cpp
 create mode 100644 src/audio_core/audio_out_manager.h
 create mode 100644 src/audio_core/audio_render_manager.cpp
 create mode 100644 src/audio_core/audio_render_manager.h
 delete mode 100644 src/audio_core/audio_renderer.cpp
 delete mode 100644 src/audio_core/audio_renderer.h
 delete mode 100644 src/audio_core/behavior_info.cpp
 delete mode 100644 src/audio_core/behavior_info.h
 delete mode 100644 src/audio_core/buffer.h
 delete mode 100644 src/audio_core/codec.cpp
 delete mode 100644 src/audio_core/codec.h
 delete mode 100644 src/audio_core/command_generator.cpp
 delete mode 100644 src/audio_core/command_generator.h
 delete mode 100644 src/audio_core/common.h
 create mode 100644 src/audio_core/common/audio_renderer_parameter.h
 create mode 100644 src/audio_core/common/common.h
 create mode 100644 src/audio_core/common/feature_support.h
 create mode 100644 src/audio_core/common/wave_buffer.h
 create mode 100644 src/audio_core/common/workbuffer_allocator.h
 delete mode 100644 src/audio_core/cubeb_sink.cpp
 delete mode 100644 src/audio_core/cubeb_sink.h
 delete mode 100644 src/audio_core/delay_line.cpp
 delete mode 100644 src/audio_core/delay_line.h
 create mode 100644 src/audio_core/device/audio_buffer.h
 create mode 100644 src/audio_core/device/audio_buffers.h
 create mode 100644 src/audio_core/device/device_session.cpp
 create mode 100644 src/audio_core/device/device_session.h
 delete mode 100644 src/audio_core/effect_context.cpp
 delete mode 100644 src/audio_core/effect_context.h
 create mode 100644 src/audio_core/in/audio_in.cpp
 create mode 100644 src/audio_core/in/audio_in.h
 create mode 100644 src/audio_core/in/audio_in_system.cpp
 create mode 100644 src/audio_core/in/audio_in_system.h
 delete mode 100644 src/audio_core/info_updater.cpp
 delete mode 100644 src/audio_core/info_updater.h
 delete mode 100644 src/audio_core/memory_pool.cpp
 delete mode 100644 src/audio_core/memory_pool.h
 delete mode 100644 src/audio_core/mix_context.cpp
 delete mode 100644 src/audio_core/mix_context.h
 delete mode 100644 src/audio_core/null_sink.h
 create mode 100644 src/audio_core/out/audio_out.cpp
 create mode 100644 src/audio_core/out/audio_out.h
 create mode 100644 src/audio_core/out/audio_out_system.cpp
 create mode 100644 src/audio_core/out/audio_out_system.h
 create mode 100644 src/audio_core/renderer/adsp/adsp.cpp
 create mode 100644 src/audio_core/renderer/adsp/adsp.h
 create mode 100644 src/audio_core/renderer/adsp/audio_renderer.cpp
 create mode 100644 src/audio_core/renderer/adsp/audio_renderer.h
 create mode 100644 src/audio_core/renderer/adsp/command_buffer.h
 create mode 100644 src/audio_core/renderer/adsp/command_list_processor.cpp
 create mode 100644 src/audio_core/renderer/adsp/command_list_processor.h
 create mode 100644 src/audio_core/renderer/audio_device.cpp
 create mode 100644 src/audio_core/renderer/audio_device.h
 create mode 100644 src/audio_core/renderer/audio_renderer.cpp
 create mode 100644 src/audio_core/renderer/audio_renderer.h
 create mode 100644 src/audio_core/renderer/behavior/behavior_info.cpp
 create mode 100644 src/audio_core/renderer/behavior/behavior_info.h
 create mode 100644 src/audio_core/renderer/behavior/info_updater.cpp
 create mode 100644 src/audio_core/renderer/behavior/info_updater.h
 create mode 100644 src/audio_core/renderer/command/command_buffer.cpp
 create mode 100644 src/audio_core/renderer/command/command_buffer.h
 create mode 100644 src/audio_core/renderer/command/command_generator.cpp
 create mode 100644 src/audio_core/renderer/command/command_generator.h
 create mode 100644 src/audio_core/renderer/command/command_list_header.h
 create mode 100644 src/audio_core/renderer/command/command_processing_time_estimator.cpp
 create mode 100644 src/audio_core/renderer/command/command_processing_time_estimator.h
 create mode 100644 src/audio_core/renderer/command/commands.h
 create mode 100644 src/audio_core/renderer/command/data_source/adpcm.cpp
 create mode 100644 src/audio_core/renderer/command/data_source/adpcm.h
 create mode 100644 src/audio_core/renderer/command/data_source/decode.cpp
 create mode 100644 src/audio_core/renderer/command/data_source/decode.h
 create mode 100644 src/audio_core/renderer/command/data_source/pcm_float.cpp
 create mode 100644 src/audio_core/renderer/command/data_source/pcm_float.h
 create mode 100644 src/audio_core/renderer/command/data_source/pcm_int16.cpp
 create mode 100644 src/audio_core/renderer/command/data_source/pcm_int16.h
 create mode 100644 src/audio_core/renderer/command/effect/aux_.cpp
 create mode 100644 src/audio_core/renderer/command/effect/aux_.h
 create mode 100644 src/audio_core/renderer/command/effect/biquad_filter.cpp
 create mode 100644 src/audio_core/renderer/command/effect/biquad_filter.h
 create mode 100644 src/audio_core/renderer/command/effect/capture.cpp
 create mode 100644 src/audio_core/renderer/command/effect/capture.h
 create mode 100644 src/audio_core/renderer/command/effect/compressor.cpp
 create mode 100644 src/audio_core/renderer/command/effect/compressor.h
 create mode 100644 src/audio_core/renderer/command/effect/delay.cpp
 create mode 100644 src/audio_core/renderer/command/effect/delay.h
 create mode 100644 src/audio_core/renderer/command/effect/i3dl2_reverb.cpp
 create mode 100644 src/audio_core/renderer/command/effect/i3dl2_reverb.h
 create mode 100644 src/audio_core/renderer/command/effect/light_limiter.cpp
 create mode 100644 src/audio_core/renderer/command/effect/light_limiter.h
 create mode 100644 src/audio_core/renderer/command/effect/multi_tap_biquad_filter.cpp
 create mode 100644 src/audio_core/renderer/command/effect/multi_tap_biquad_filter.h
 create mode 100644 src/audio_core/renderer/command/effect/reverb.cpp
 create mode 100644 src/audio_core/renderer/command/effect/reverb.h
 create mode 100644 src/audio_core/renderer/command/icommand.h
 create mode 100644 src/audio_core/renderer/command/mix/clear_mix.cpp
 create mode 100644 src/audio_core/renderer/command/mix/clear_mix.h
 create mode 100644 src/audio_core/renderer/command/mix/copy_mix.cpp
 create mode 100644 src/audio_core/renderer/command/mix/copy_mix.h
 create mode 100644 src/audio_core/renderer/command/mix/depop_for_mix_buffers.cpp
 create mode 100644 src/audio_core/renderer/command/mix/depop_for_mix_buffers.h
 create mode 100644 src/audio_core/renderer/command/mix/depop_prepare.cpp
 create mode 100644 src/audio_core/renderer/command/mix/depop_prepare.h
 create mode 100644 src/audio_core/renderer/command/mix/mix.cpp
 create mode 100644 src/audio_core/renderer/command/mix/mix.h
 create mode 100644 src/audio_core/renderer/command/mix/mix_ramp.cpp
 create mode 100644 src/audio_core/renderer/command/mix/mix_ramp.h
 create mode 100644 src/audio_core/renderer/command/mix/mix_ramp_grouped.cpp
 create mode 100644 src/audio_core/renderer/command/mix/mix_ramp_grouped.h
 create mode 100644 src/audio_core/renderer/command/mix/volume.cpp
 create mode 100644 src/audio_core/renderer/command/mix/volume.h
 create mode 100644 src/audio_core/renderer/command/mix/volume_ramp.cpp
 create mode 100644 src/audio_core/renderer/command/mix/volume_ramp.h
 create mode 100644 src/audio_core/renderer/command/performance/performance.cpp
 create mode 100644 src/audio_core/renderer/command/performance/performance.h
 create mode 100644 src/audio_core/renderer/command/resample/downmix_6ch_to_2ch.cpp
 create mode 100644 src/audio_core/renderer/command/resample/downmix_6ch_to_2ch.h
 create mode 100644 src/audio_core/renderer/command/resample/resample.cpp
 create mode 100644 src/audio_core/renderer/command/resample/resample.h
 create mode 100644 src/audio_core/renderer/command/resample/upsample.cpp
 create mode 100644 src/audio_core/renderer/command/resample/upsample.h
 create mode 100644 src/audio_core/renderer/command/sink/circular_buffer.cpp
 create mode 100644 src/audio_core/renderer/command/sink/circular_buffer.h
 create mode 100644 src/audio_core/renderer/command/sink/device.cpp
 create mode 100644 src/audio_core/renderer/command/sink/device.h
 create mode 100644 src/audio_core/renderer/effect/aux_.cpp
 create mode 100644 src/audio_core/renderer/effect/aux_.h
 create mode 100644 src/audio_core/renderer/effect/biquad_filter.cpp
 create mode 100644 src/audio_core/renderer/effect/biquad_filter.h
 create mode 100644 src/audio_core/renderer/effect/buffer_mixer.cpp
 create mode 100644 src/audio_core/renderer/effect/buffer_mixer.h
 create mode 100644 src/audio_core/renderer/effect/capture.cpp
 create mode 100644 src/audio_core/renderer/effect/capture.h
 create mode 100644 src/audio_core/renderer/effect/compressor.cpp
 create mode 100644 src/audio_core/renderer/effect/compressor.h
 create mode 100644 src/audio_core/renderer/effect/delay.cpp
 create mode 100644 src/audio_core/renderer/effect/delay.h
 create mode 100644 src/audio_core/renderer/effect/effect_context.cpp
 create mode 100644 src/audio_core/renderer/effect/effect_context.h
 create mode 100644 src/audio_core/renderer/effect/effect_info_base.h
 create mode 100644 src/audio_core/renderer/effect/effect_reset.h
 create mode 100644 src/audio_core/renderer/effect/effect_result_state.h
 create mode 100644 src/audio_core/renderer/effect/i3dl2.cpp
 create mode 100644 src/audio_core/renderer/effect/i3dl2.h
 create mode 100644 src/audio_core/renderer/effect/light_limiter.cpp
 create mode 100644 src/audio_core/renderer/effect/light_limiter.h
 create mode 100644 src/audio_core/renderer/effect/reverb.cpp
 create mode 100644 src/audio_core/renderer/effect/reverb.h
 create mode 100644 src/audio_core/renderer/memory/address_info.h
 create mode 100644 src/audio_core/renderer/memory/memory_pool_info.cpp
 create mode 100644 src/audio_core/renderer/memory/memory_pool_info.h
 create mode 100644 src/audio_core/renderer/memory/pool_mapper.cpp
 create mode 100644 src/audio_core/renderer/memory/pool_mapper.h
 create mode 100644 src/audio_core/renderer/mix/mix_context.cpp
 create mode 100644 src/audio_core/renderer/mix/mix_context.h
 create mode 100644 src/audio_core/renderer/mix/mix_info.cpp
 create mode 100644 src/audio_core/renderer/mix/mix_info.h
 create mode 100644 src/audio_core/renderer/nodes/bit_array.h
 create mode 100644 src/audio_core/renderer/nodes/edge_matrix.cpp
 create mode 100644 src/audio_core/renderer/nodes/edge_matrix.h
 create mode 100644 src/audio_core/renderer/nodes/node_states.cpp
 create mode 100644 src/audio_core/renderer/nodes/node_states.h
 create mode 100644 src/audio_core/renderer/performance/detail_aspect.cpp
 create mode 100644 src/audio_core/renderer/performance/detail_aspect.h
 create mode 100644 src/audio_core/renderer/performance/entry_aspect.cpp
 create mode 100644 src/audio_core/renderer/performance/entry_aspect.h
 create mode 100644 src/audio_core/renderer/performance/performance_detail.h
 create mode 100644 src/audio_core/renderer/performance/performance_entry.h
 create mode 100644 src/audio_core/renderer/performance/performance_entry_addresses.h
 create mode 100644 src/audio_core/renderer/performance/performance_frame_header.h
 create mode 100644 src/audio_core/renderer/performance/performance_manager.cpp
 create mode 100644 src/audio_core/renderer/performance/performance_manager.h
 create mode 100644 src/audio_core/renderer/sink/circular_buffer_sink_info.cpp
 create mode 100644 src/audio_core/renderer/sink/circular_buffer_sink_info.h
 create mode 100644 src/audio_core/renderer/sink/device_sink_info.cpp
 create mode 100644 src/audio_core/renderer/sink/device_sink_info.h
 create mode 100644 src/audio_core/renderer/sink/sink_context.cpp
 create mode 100644 src/audio_core/renderer/sink/sink_context.h
 create mode 100644 src/audio_core/renderer/sink/sink_info_base.cpp
 create mode 100644 src/audio_core/renderer/sink/sink_info_base.h
 create mode 100644 src/audio_core/renderer/splitter/splitter_context.cpp
 create mode 100644 src/audio_core/renderer/splitter/splitter_context.h
 create mode 100644 src/audio_core/renderer/splitter/splitter_destinations_data.cpp
 create mode 100644 src/audio_core/renderer/splitter/splitter_destinations_data.h
 create mode 100644 src/audio_core/renderer/splitter/splitter_info.cpp
 create mode 100644 src/audio_core/renderer/splitter/splitter_info.h
 create mode 100644 src/audio_core/renderer/system.cpp
 create mode 100644 src/audio_core/renderer/system.h
 create mode 100644 src/audio_core/renderer/system_manager.cpp
 create mode 100644 src/audio_core/renderer/system_manager.h
 create mode 100644 src/audio_core/renderer/upsampler/upsampler_info.cpp
 create mode 100644 src/audio_core/renderer/upsampler/upsampler_info.h
 create mode 100644 src/audio_core/renderer/upsampler/upsampler_manager.cpp
 create mode 100644 src/audio_core/renderer/upsampler/upsampler_manager.h
 create mode 100644 src/audio_core/renderer/upsampler/upsampler_state.h
 create mode 100644 src/audio_core/renderer/voice/voice_channel_resource.h
 create mode 100644 src/audio_core/renderer/voice/voice_context.cpp
 create mode 100644 src/audio_core/renderer/voice/voice_context.h
 create mode 100644 src/audio_core/renderer/voice/voice_info.cpp
 create mode 100644 src/audio_core/renderer/voice/voice_info.h
 create mode 100644 src/audio_core/renderer/voice/voice_state.h
 delete mode 100644 src/audio_core/sdl2_sink.cpp
 delete mode 100644 src/audio_core/sdl2_sink.h
 delete mode 100644 src/audio_core/sink.h
 create mode 100644 src/audio_core/sink/cubeb_sink.cpp
 create mode 100644 src/audio_core/sink/cubeb_sink.h
 create mode 100644 src/audio_core/sink/null_sink.h
 create mode 100644 src/audio_core/sink/sdl2_sink.cpp
 create mode 100644 src/audio_core/sink/sdl2_sink.h
 create mode 100644 src/audio_core/sink/sink.h
 rename src/audio_core/{ => sink}/sink_details.cpp (76%)
 create mode 100644 src/audio_core/sink/sink_details.h
 create mode 100644 src/audio_core/sink/sink_stream.h
 delete mode 100644 src/audio_core/sink_context.cpp
 delete mode 100644 src/audio_core/sink_context.h
 delete mode 100644 src/audio_core/sink_details.h
 delete mode 100644 src/audio_core/sink_stream.h
 delete mode 100644 src/audio_core/splitter_context.cpp
 delete mode 100644 src/audio_core/splitter_context.h
 delete mode 100644 src/audio_core/stream.cpp
 delete mode 100644 src/audio_core/stream.h
 delete mode 100644 src/audio_core/voice_context.cpp
 delete mode 100644 src/audio_core/voice_context.h
 create mode 100644 src/common/atomic_helpers.h
 create mode 100644 src/common/fixed_point.h
 create mode 100644 src/common/reader_writer_queue.h

diff --git a/.gitmodules b/.gitmodules
index dc92d0a4b5..ed533f8d43 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -3,7 +3,7 @@
     url = https://github.com/benhoyt/inih.git
 [submodule "cubeb"]
     path = externals/cubeb
-    url = https://github.com/kinetiknz/cubeb.git
+    url = https://github.com/mozilla/cubeb.git
 [submodule "dynarmic"]
     path = externals/dynarmic
     url = https://github.com/MerryMage/dynarmic.git
diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt
index 89575a53ef..2971c42a2f 100644
--- a/src/audio_core/CMakeLists.txt
+++ b/src/audio_core/CMakeLists.txt
@@ -1,54 +1,218 @@
 add_library(audio_core STATIC
-    algorithm/filter.cpp
-    algorithm/filter.h
-    algorithm/interpolate.cpp
-    algorithm/interpolate.h
-    audio_out.cpp
-    audio_out.h
-    audio_renderer.cpp
-    audio_renderer.h
-    behavior_info.cpp
-    behavior_info.h
-    buffer.h
-    codec.cpp
-    codec.h
-    command_generator.cpp
-    command_generator.h
-    common.h
-    delay_line.cpp
-    delay_line.h
-    effect_context.cpp
-    effect_context.h
-    info_updater.cpp
-    info_updater.h
-    memory_pool.cpp
-    memory_pool.h
-    mix_context.cpp
-    mix_context.h
-    null_sink.h
-    sink.h
-    sink_context.cpp
-    sink_context.h
-    sink_details.cpp
-    sink_details.h
-    sink_stream.h
-    splitter_context.cpp
-    splitter_context.h
-    stream.cpp
-    stream.h
-    voice_context.cpp
-    voice_context.h
-
-    $<$<BOOL:${ENABLE_CUBEB}>:cubeb_sink.cpp cubeb_sink.h>
-    $<$<BOOL:${ENABLE_SDL2}>:sdl2_sink.cpp sdl2_sink.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
+    out/audio_out.cpp
+    out/audio_out.h
+    out/audio_out_system.cpp
+    out/audio_out_system.h
+    renderer/adsp/adsp.cpp
+    renderer/adsp/adsp.h
+    renderer/adsp/audio_renderer.cpp
+    renderer/adsp/audio_renderer.h
+    renderer/adsp/command_buffer.h
+    renderer/adsp/command_list_processor.cpp
+    renderer/adsp/command_list_processor.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/cubeb_sink.cpp
+    sink/cubeb_sink.h
+    sink/null_sink.h
+    sink/sdl2_sink.cpp
+    sink/sdl2_sink.h
+    sink/sink.h
+    sink/sink_details.cpp
+    sink/sink_details.h
+    sink/sink_stream.h
 )
 
 create_target_directory_groups(audio_core)
 
-if (NOT MSVC)
+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
+        /we4456 # Declaration of 'identifier' hides previous local declaration
+        /we4457 # Declaration of 'identifier' hides function parameter
+        /we4458 # Declaration of 'identifier' hides class member
+        /we4459 # Declaration of 'identifier' hides global declaration
+    )
+else()
     target_compile_options(audio_core PRIVATE
         -Werror=conversion
         -Werror=ignored-qualifiers
+        -Werror=shadow
+        -Werror=unused-variable
 
         $<$<CXX_COMPILER_ID:GNU>:-Werror=unused-but-set-parameter>
         $<$<CXX_COMPILER_ID:GNU>:-Werror=unused-but-set-variable>
@@ -58,6 +222,9 @@ if (NOT MSVC)
 endif()
 
 target_link_libraries(audio_core PUBLIC common core)
+if (ARCHITECTURE_x86_64)
+    target_link_libraries(audio_core PRIVATE dynarmic)
+endif()
 
 if(ENABLE_CUBEB)
     target_link_libraries(audio_core PRIVATE cubeb)
diff --git a/src/audio_core/algorithm/filter.cpp b/src/audio_core/algorithm/filter.cpp
deleted file mode 100644
index 96e37991f7..0000000000
--- a/src/audio_core/algorithm/filter.cpp
+++ /dev/null
@@ -1,79 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#define _USE_MATH_DEFINES
-
-#include <algorithm>
-#include <array>
-#include <cmath>
-#include <vector>
-#include "audio_core/algorithm/filter.h"
-#include "common/common_types.h"
-
-namespace AudioCore {
-
-Filter Filter::LowPass(double cutoff, double Q) {
-    const double w0 = 2.0 * M_PI * cutoff;
-    const double sin_w0 = std::sin(w0);
-    const double cos_w0 = std::cos(w0);
-    const double alpha = sin_w0 / (2 * Q);
-
-    const double a0 = 1 + alpha;
-    const double a1 = -2.0 * cos_w0;
-    const double a2 = 1 - alpha;
-    const double b0 = 0.5 * (1 - cos_w0);
-    const double b1 = 1.0 * (1 - cos_w0);
-    const double b2 = 0.5 * (1 - cos_w0);
-
-    return {a0, a1, a2, b0, b1, b2};
-}
-
-Filter::Filter() : Filter(1.0, 0.0, 0.0, 1.0, 0.0, 0.0) {}
-
-Filter::Filter(double a0_, double a1_, double a2_, double b0_, double b1_, double b2_)
-    : a1(a1_ / a0_), a2(a2_ / a0_), b0(b0_ / a0_), b1(b1_ / a0_), b2(b2_ / a0_) {}
-
-void Filter::Process(std::vector<s16>& signal) {
-    const std::size_t num_frames = signal.size() / 2;
-    for (std::size_t i = 0; i < num_frames; i++) {
-        std::rotate(in.begin(), in.end() - 1, in.end());
-        std::rotate(out.begin(), out.end() - 1, out.end());
-
-        for (std::size_t ch = 0; ch < channel_count; ch++) {
-            in[0][ch] = signal[i * channel_count + ch];
-
-            out[0][ch] = b0 * in[0][ch] + b1 * in[1][ch] + b2 * in[2][ch] - a1 * out[1][ch] -
-                         a2 * out[2][ch];
-
-            signal[i * 2 + ch] = static_cast<s16>(std::clamp(out[0][ch], -32768.0, 32767.0));
-        }
-    }
-}
-
-/// Calculates the appropriate Q for each biquad in a cascading filter.
-/// @param total_count The total number of biquads to be cascaded.
-/// @param index 0-index of the biquad to calculate the Q value for.
-static double CascadingBiquadQ(std::size_t total_count, std::size_t index) {
-    const auto pole =
-        M_PI * static_cast<double>(2 * index + 1) / (4.0 * static_cast<double>(total_count));
-    return 1.0 / (2.0 * std::cos(pole));
-}
-
-CascadingFilter CascadingFilter::LowPass(double cutoff, std::size_t cascade_size) {
-    std::vector<Filter> cascade(cascade_size);
-    for (std::size_t i = 0; i < cascade_size; i++) {
-        cascade[i] = Filter::LowPass(cutoff, CascadingBiquadQ(cascade_size, i));
-    }
-    return CascadingFilter{std::move(cascade)};
-}
-
-CascadingFilter::CascadingFilter() = default;
-CascadingFilter::CascadingFilter(std::vector<Filter> filters_) : filters(std::move(filters_)) {}
-
-void CascadingFilter::Process(std::vector<s16>& signal) {
-    for (auto& filter : filters) {
-        filter.Process(signal);
-    }
-}
-
-} // namespace AudioCore
diff --git a/src/audio_core/algorithm/filter.h b/src/audio_core/algorithm/filter.h
deleted file mode 100644
index 2586f00799..0000000000
--- a/src/audio_core/algorithm/filter.h
+++ /dev/null
@@ -1,61 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include <array>
-#include <vector>
-#include "common/common_types.h"
-
-namespace AudioCore {
-
-/// Digital biquad filter:
-///
-///          b0 + b1 z^-1 + b2 z^-2
-///  H(z) = ------------------------
-///          a0 + a1 z^-1 + b2 z^-2
-class Filter {
-public:
-    /// Creates a low-pass filter.
-    /// @param cutoff Determines the cutoff frequency. A value from 0.0 to 1.0.
-    /// @param Q Determines the quality factor of this filter.
-    static Filter LowPass(double cutoff, double Q = 0.7071);
-
-    /// Passthrough filter.
-    Filter();
-
-    Filter(double a0_, double a1_, double a2_, double b0_, double b1_, double b2_);
-
-    void Process(std::vector<s16>& signal);
-
-private:
-    static constexpr std::size_t channel_count = 2;
-
-    /// Coefficients are in normalized form (a0 = 1.0).
-    double a1, a2, b0, b1, b2;
-    /// Input History
-    std::array<std::array<double, channel_count>, 3> in;
-    /// Output History
-    std::array<std::array<double, channel_count>, 3> out;
-};
-
-/// Cascade filters to build up higher-order filters from lower-order ones.
-class CascadingFilter {
-public:
-    /// Creates a cascading low-pass filter.
-    /// @param cutoff Determines the cutoff frequency. A value from 0.0 to 1.0.
-    /// @param cascade_size Number of biquads in cascade.
-    static CascadingFilter LowPass(double cutoff, std::size_t cascade_size);
-
-    /// Passthrough.
-    CascadingFilter();
-
-    explicit CascadingFilter(std::vector<Filter> filters_);
-
-    void Process(std::vector<s16>& signal);
-
-private:
-    std::vector<Filter> filters;
-};
-
-} // namespace AudioCore
diff --git a/src/audio_core/algorithm/interpolate.cpp b/src/audio_core/algorithm/interpolate.cpp
deleted file mode 100644
index d2a4cd53f5..0000000000
--- a/src/audio_core/algorithm/interpolate.cpp
+++ /dev/null
@@ -1,232 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#define _USE_MATH_DEFINES
-
-#include <algorithm>
-#include <climits>
-#include <cmath>
-#include <vector>
-
-#include "audio_core/algorithm/interpolate.h"
-#include "common/common_types.h"
-#include "common/logging/log.h"
-
-namespace AudioCore {
-
-constexpr std::array<s16, 512> curve_lut0{
-    6600,  19426, 6722,  3,     6479,  19424, 6845,  9,     6359,  19419, 6968,  15,    6239,
-    19412, 7093,  22,    6121,  19403, 7219,  28,    6004,  19391, 7345,  34,    5888,  19377,
-    7472,  41,    5773,  19361, 7600,  48,    5659,  19342, 7728,  55,    5546,  19321, 7857,
-    62,    5434,  19298, 7987,  69,    5323,  19273, 8118,  77,    5213,  19245, 8249,  84,
-    5104,  19215, 8381,  92,    4997,  19183, 8513,  101,   4890,  19148, 8646,  109,   4785,
-    19112, 8780,  118,   4681,  19073, 8914,  127,   4579,  19031, 9048,  137,   4477,  18988,
-    9183,  147,   4377,  18942, 9318,  157,   4277,  18895, 9454,  168,   4179,  18845, 9590,
-    179,   4083,  18793, 9726,  190,   3987,  18738, 9863,  202,   3893,  18682, 10000, 215,
-    3800,  18624, 10137, 228,   3709,  18563, 10274, 241,   3618,  18500, 10411, 255,   3529,
-    18436, 10549, 270,   3441,  18369, 10687, 285,   3355,  18300, 10824, 300,   3269,  18230,
-    10962, 317,   3186,  18157, 11100, 334,   3103,  18082, 11238, 351,   3022,  18006, 11375,
-    369,   2942,  17927, 11513, 388,   2863,  17847, 11650, 408,   2785,  17765, 11788, 428,
-    2709,  17681, 11925, 449,   2635,  17595, 12062, 471,   2561,  17507, 12198, 494,   2489,
-    17418, 12334, 517,   2418,  17327, 12470, 541,   2348,  17234, 12606, 566,   2280,  17140,
-    12741, 592,   2213,  17044, 12876, 619,   2147,  16946, 13010, 647,   2083,  16846, 13144,
-    675,   2020,  16745, 13277, 704,   1958,  16643, 13409, 735,   1897,  16539, 13541, 766,
-    1838,  16434, 13673, 798,   1780,  16327, 13803, 832,   1723,  16218, 13933, 866,   1667,
-    16109, 14062, 901,   1613,  15998, 14191, 937,   1560,  15885, 14318, 975,   1508,  15772,
-    14445, 1013,  1457,  15657, 14571, 1052,  1407,  15540, 14695, 1093,  1359,  15423, 14819,
-    1134,  1312,  15304, 14942, 1177,  1266,  15185, 15064, 1221,  1221,  15064, 15185, 1266,
-    1177,  14942, 15304, 1312,  1134,  14819, 15423, 1359,  1093,  14695, 15540, 1407,  1052,
-    14571, 15657, 1457,  1013,  14445, 15772, 1508,  975,   14318, 15885, 1560,  937,   14191,
-    15998, 1613,  901,   14062, 16109, 1667,  866,   13933, 16218, 1723,  832,   13803, 16327,
-    1780,  798,   13673, 16434, 1838,  766,   13541, 16539, 1897,  735,   13409, 16643, 1958,
-    704,   13277, 16745, 2020,  675,   13144, 16846, 2083,  647,   13010, 16946, 2147,  619,
-    12876, 17044, 2213,  592,   12741, 17140, 2280,  566,   12606, 17234, 2348,  541,   12470,
-    17327, 2418,  517,   12334, 17418, 2489,  494,   12198, 17507, 2561,  471,   12062, 17595,
-    2635,  449,   11925, 17681, 2709,  428,   11788, 17765, 2785,  408,   11650, 17847, 2863,
-    388,   11513, 17927, 2942,  369,   11375, 18006, 3022,  351,   11238, 18082, 3103,  334,
-    11100, 18157, 3186,  317,   10962, 18230, 3269,  300,   10824, 18300, 3355,  285,   10687,
-    18369, 3441,  270,   10549, 18436, 3529,  255,   10411, 18500, 3618,  241,   10274, 18563,
-    3709,  228,   10137, 18624, 3800,  215,   10000, 18682, 3893,  202,   9863,  18738, 3987,
-    190,   9726,  18793, 4083,  179,   9590,  18845, 4179,  168,   9454,  18895, 4277,  157,
-    9318,  18942, 4377,  147,   9183,  18988, 4477,  137,   9048,  19031, 4579,  127,   8914,
-    19073, 4681,  118,   8780,  19112, 4785,  109,   8646,  19148, 4890,  101,   8513,  19183,
-    4997,  92,    8381,  19215, 5104,  84,    8249,  19245, 5213,  77,    8118,  19273, 5323,
-    69,    7987,  19298, 5434,  62,    7857,  19321, 5546,  55,    7728,  19342, 5659,  48,
-    7600,  19361, 5773,  41,    7472,  19377, 5888,  34,    7345,  19391, 6004,  28,    7219,
-    19403, 6121,  22,    7093,  19412, 6239,  15,    6968,  19419, 6359,  9,     6845,  19424,
-    6479,  3,     6722,  19426, 6600};
-
-constexpr std::array<s16, 512> curve_lut1{
-    -68,   32639, 69,    -5,    -200,  32630, 212,   -15,   -328,  32613, 359,   -26,   -450,
-    32586, 512,   -36,   -568,  32551, 669,   -47,   -680,  32507, 832,   -58,   -788,  32454,
-    1000,  -69,   -891,  32393, 1174,  -80,   -990,  32323, 1352,  -92,   -1084, 32244, 1536,
-    -103,  -1173, 32157, 1724,  -115,  -1258, 32061, 1919,  -128,  -1338, 31956, 2118,  -140,
-    -1414, 31844, 2322,  -153,  -1486, 31723, 2532,  -167,  -1554, 31593, 2747,  -180,  -1617,
-    31456, 2967,  -194,  -1676, 31310, 3192,  -209,  -1732, 31157, 3422,  -224,  -1783, 30995,
-    3657,  -240,  -1830, 30826, 3897,  -256,  -1874, 30649, 4143,  -272,  -1914, 30464, 4393,
-    -289,  -1951, 30272, 4648,  -307,  -1984, 30072, 4908,  -325,  -2014, 29866, 5172,  -343,
-    -2040, 29652, 5442,  -362,  -2063, 29431, 5716,  -382,  -2083, 29203, 5994,  -403,  -2100,
-    28968, 6277,  -424,  -2114, 28727, 6565,  -445,  -2125, 28480, 6857,  -468,  -2133, 28226,
-    7153,  -490,  -2139, 27966, 7453,  -514,  -2142, 27700, 7758,  -538,  -2142, 27428, 8066,
-    -563,  -2141, 27151, 8378,  -588,  -2136, 26867, 8694,  -614,  -2130, 26579, 9013,  -641,
-    -2121, 26285, 9336,  -668,  -2111, 25987, 9663,  -696,  -2098, 25683, 9993,  -724,  -2084,
-    25375, 10326, -753,  -2067, 25063, 10662, -783,  -2049, 24746, 11000, -813,  -2030, 24425,
-    11342, -844,  -2009, 24100, 11686, -875,  -1986, 23771, 12033, -907,  -1962, 23438, 12382,
-    -939,  -1937, 23103, 12733, -972,  -1911, 22764, 13086, -1005, -1883, 22422, 13441, -1039,
-    -1855, 22077, 13798, -1072, -1825, 21729, 14156, -1107, -1795, 21380, 14516, -1141, -1764,
-    21027, 14877, -1176, -1732, 20673, 15239, -1211, -1700, 20317, 15602, -1246, -1667, 19959,
-    15965, -1282, -1633, 19600, 16329, -1317, -1599, 19239, 16694, -1353, -1564, 18878, 17058,
-    -1388, -1530, 18515, 17423, -1424, -1495, 18151, 17787, -1459, -1459, 17787, 18151, -1495,
-    -1424, 17423, 18515, -1530, -1388, 17058, 18878, -1564, -1353, 16694, 19239, -1599, -1317,
-    16329, 19600, -1633, -1282, 15965, 19959, -1667, -1246, 15602, 20317, -1700, -1211, 15239,
-    20673, -1732, -1176, 14877, 21027, -1764, -1141, 14516, 21380, -1795, -1107, 14156, 21729,
-    -1825, -1072, 13798, 22077, -1855, -1039, 13441, 22422, -1883, -1005, 13086, 22764, -1911,
-    -972,  12733, 23103, -1937, -939,  12382, 23438, -1962, -907,  12033, 23771, -1986, -875,
-    11686, 24100, -2009, -844,  11342, 24425, -2030, -813,  11000, 24746, -2049, -783,  10662,
-    25063, -2067, -753,  10326, 25375, -2084, -724,  9993,  25683, -2098, -696,  9663,  25987,
-    -2111, -668,  9336,  26285, -2121, -641,  9013,  26579, -2130, -614,  8694,  26867, -2136,
-    -588,  8378,  27151, -2141, -563,  8066,  27428, -2142, -538,  7758,  27700, -2142, -514,
-    7453,  27966, -2139, -490,  7153,  28226, -2133, -468,  6857,  28480, -2125, -445,  6565,
-    28727, -2114, -424,  6277,  28968, -2100, -403,  5994,  29203, -2083, -382,  5716,  29431,
-    -2063, -362,  5442,  29652, -2040, -343,  5172,  29866, -2014, -325,  4908,  30072, -1984,
-    -307,  4648,  30272, -1951, -289,  4393,  30464, -1914, -272,  4143,  30649, -1874, -256,
-    3897,  30826, -1830, -240,  3657,  30995, -1783, -224,  3422,  31157, -1732, -209,  3192,
-    31310, -1676, -194,  2967,  31456, -1617, -180,  2747,  31593, -1554, -167,  2532,  31723,
-    -1486, -153,  2322,  31844, -1414, -140,  2118,  31956, -1338, -128,  1919,  32061, -1258,
-    -115,  1724,  32157, -1173, -103,  1536,  32244, -1084, -92,   1352,  32323, -990,  -80,
-    1174,  32393, -891,  -69,   1000,  32454, -788,  -58,   832,   32507, -680,  -47,   669,
-    32551, -568,  -36,   512,   32586, -450,  -26,   359,   32613, -328,  -15,   212,   32630,
-    -200,  -5,    69,    32639, -68};
-
-constexpr std::array<s16, 512> curve_lut2{
-    3195,  26287, 3329,  -32,   3064,  26281, 3467,  -34,   2936,  26270, 3608,  -38,   2811,
-    26253, 3751,  -42,   2688,  26230, 3897,  -46,   2568,  26202, 4046,  -50,   2451,  26169,
-    4199,  -54,   2338,  26130, 4354,  -58,   2227,  26085, 4512,  -63,   2120,  26035, 4673,
-    -67,   2015,  25980, 4837,  -72,   1912,  25919, 5004,  -76,   1813,  25852, 5174,  -81,
-    1716,  25780, 5347,  -87,   1622,  25704, 5522,  -92,   1531,  25621, 5701,  -98,   1442,
-    25533, 5882,  -103,  1357,  25440, 6066,  -109,  1274,  25342, 6253,  -115,  1193,  25239,
-    6442,  -121,  1115,  25131, 6635,  -127,  1040,  25018, 6830,  -133,  967,   24899, 7027,
-    -140,  897,   24776, 7227,  -146,  829,   24648, 7430,  -153,  764,   24516, 7635,  -159,
-    701,   24379, 7842,  -166,  641,   24237, 8052,  -174,  583,   24091, 8264,  -181,  526,
-    23940, 8478,  -187,  472,   23785, 8695,  -194,  420,   23626, 8914,  -202,  371,   23462,
-    9135,  -209,  324,   23295, 9358,  -215,  279,   23123, 9583,  -222,  236,   22948, 9809,
-    -230,  194,   22769, 10038, -237,  154,   22586, 10269, -243,  117,   22399, 10501, -250,
-    81,    22208, 10735, -258,  47,    22015, 10970, -265,  15,    21818, 11206, -271,  -16,
-    21618, 11444, -277,  -44,   21415, 11684, -283,  -71,   21208, 11924, -290,  -97,   20999,
-    12166, -296,  -121,  20786, 12409, -302,  -143,  20571, 12653, -306,  -163,  20354, 12898,
-    -311,  -183,  20134, 13143, -316,  -201,  19911, 13389, -321,  -218,  19686, 13635, -325,
-    -234,  19459, 13882, -328,  -248,  19230, 14130, -332,  -261,  18998, 14377, -335,  -273,
-    18765, 14625, -337,  -284,  18531, 14873, -339,  -294,  18295, 15121, -341,  -302,  18057,
-    15369, -341,  -310,  17817, 15617, -341,  -317,  17577, 15864, -340,  -323,  17335, 16111,
-    -340,  -328,  17092, 16357, -338,  -332,  16848, 16603, -336,  -336,  16603, 16848, -332,
-    -338,  16357, 17092, -328,  -340,  16111, 17335, -323,  -340,  15864, 17577, -317,  -341,
-    15617, 17817, -310,  -341,  15369, 18057, -302,  -341,  15121, 18295, -294,  -339,  14873,
-    18531, -284,  -337,  14625, 18765, -273,  -335,  14377, 18998, -261,  -332,  14130, 19230,
-    -248,  -328,  13882, 19459, -234,  -325,  13635, 19686, -218,  -321,  13389, 19911, -201,
-    -316,  13143, 20134, -183,  -311,  12898, 20354, -163,  -306,  12653, 20571, -143,  -302,
-    12409, 20786, -121,  -296,  12166, 20999, -97,   -290,  11924, 21208, -71,   -283,  11684,
-    21415, -44,   -277,  11444, 21618, -16,   -271,  11206, 21818, 15,    -265,  10970, 22015,
-    47,    -258,  10735, 22208, 81,    -250,  10501, 22399, 117,   -243,  10269, 22586, 154,
-    -237,  10038, 22769, 194,   -230,  9809,  22948, 236,   -222,  9583,  23123, 279,   -215,
-    9358,  23295, 324,   -209,  9135,  23462, 371,   -202,  8914,  23626, 420,   -194,  8695,
-    23785, 472,   -187,  8478,  23940, 526,   -181,  8264,  24091, 583,   -174,  8052,  24237,
-    641,   -166,  7842,  24379, 701,   -159,  7635,  24516, 764,   -153,  7430,  24648, 829,
-    -146,  7227,  24776, 897,   -140,  7027,  24899, 967,   -133,  6830,  25018, 1040,  -127,
-    6635,  25131, 1115,  -121,  6442,  25239, 1193,  -115,  6253,  25342, 1274,  -109,  6066,
-    25440, 1357,  -103,  5882,  25533, 1442,  -98,   5701,  25621, 1531,  -92,   5522,  25704,
-    1622,  -87,   5347,  25780, 1716,  -81,   5174,  25852, 1813,  -76,   5004,  25919, 1912,
-    -72,   4837,  25980, 2015,  -67,   4673,  26035, 2120,  -63,   4512,  26085, 2227,  -58,
-    4354,  26130, 2338,  -54,   4199,  26169, 2451,  -50,   4046,  26202, 2568,  -46,   3897,
-    26230, 2688,  -42,   3751,  26253, 2811,  -38,   3608,  26270, 2936,  -34,   3467,  26281,
-    3064,  -32,   3329,  26287, 3195};
-
-std::vector<s16> Interpolate(InterpolationState& state, std::vector<s16> input, double ratio) {
-    if (input.size() < 2)
-        return {};
-
-    if (ratio <= 0) {
-        LOG_ERROR(Audio, "Nonsensical interpolation ratio {}", ratio);
-        return input;
-    }
-
-    const s32 step{static_cast<s32>(ratio * 0x8000)};
-    const std::array<s16, 512>& lut = [step] {
-        if (step > 0xaaaa) {
-            return curve_lut0;
-        }
-        if (step <= 0x8000) {
-            return curve_lut1;
-        }
-        return curve_lut2;
-    }();
-
-    const std::size_t num_frames{input.size() / 2};
-
-    std::vector<s16> output;
-    output.reserve(static_cast<std::size_t>(static_cast<double>(input.size()) / ratio +
-                                            InterpolationState::taps));
-
-    for (std::size_t frame{}; frame < num_frames; ++frame) {
-        const std::size_t lut_index{(state.fraction >> 8) * InterpolationState::taps};
-
-        std::rotate(state.history.begin(), state.history.end() - 1, state.history.end());
-        state.history[0][0] = input[frame * 2 + 0];
-        state.history[0][1] = input[frame * 2 + 1];
-
-        while (state.position <= 1.0) {
-            const s32 left{state.history[0][0] * lut[lut_index + 0] +
-                           state.history[1][0] * lut[lut_index + 1] +
-                           state.history[2][0] * lut[lut_index + 2] +
-                           state.history[3][0] * lut[lut_index + 3]};
-            const s32 right{state.history[0][1] * lut[lut_index + 0] +
-                            state.history[1][1] * lut[lut_index + 1] +
-                            state.history[2][1] * lut[lut_index + 2] +
-                            state.history[3][1] * lut[lut_index + 3]};
-            const s32 new_offset{state.fraction + step};
-
-            state.fraction = new_offset & 0x7fff;
-
-            output.emplace_back(static_cast<s16>(std::clamp(left >> 15, SHRT_MIN, SHRT_MAX)));
-            output.emplace_back(static_cast<s16>(std::clamp(right >> 15, SHRT_MIN, SHRT_MAX)));
-
-            state.position += ratio;
-        }
-        state.position -= 1.0;
-    }
-
-    return output;
-}
-
-void Resample(s32* output, const s32* input, s32 pitch, s32& fraction, std::size_t sample_count) {
-    const std::array<s16, 512>& lut = [pitch] {
-        if (pitch > 0xaaaa) {
-            return curve_lut0;
-        }
-        if (pitch <= 0x8000) {
-            return curve_lut1;
-        }
-        return curve_lut2;
-    }();
-
-    std::size_t index{};
-
-    for (std::size_t i = 0; i < sample_count; i++) {
-        const std::size_t lut_index{(static_cast<std::size_t>(fraction) >> 8) * 4};
-        const auto l0 = lut[lut_index + 0];
-        const auto l1 = lut[lut_index + 1];
-        const auto l2 = lut[lut_index + 2];
-        const auto l3 = lut[lut_index + 3];
-
-        const auto s0 = static_cast<s32>(input[index + 0]);
-        const auto s1 = static_cast<s32>(input[index + 1]);
-        const auto s2 = static_cast<s32>(input[index + 2]);
-        const auto s3 = static_cast<s32>(input[index + 3]);
-
-        output[i] = (l0 * s0 + l1 * s1 + l2 * s2 + l3 * s3) >> 15;
-        fraction += pitch;
-        index += (fraction >> 15);
-        fraction &= 0x7fff;
-    }
-}
-
-} // namespace AudioCore
diff --git a/src/audio_core/algorithm/interpolate.h b/src/audio_core/algorithm/interpolate.h
deleted file mode 100644
index 5e59f4d709..0000000000
--- a/src/audio_core/algorithm/interpolate.h
+++ /dev/null
@@ -1,43 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include <array>
-#include <vector>
-
-#include "common/common_types.h"
-
-namespace AudioCore {
-
-struct InterpolationState {
-    static constexpr std::size_t taps{4};
-    static constexpr std::size_t history_size{taps * 2 - 1};
-    std::array<std::array<s16, 2>, history_size> history{};
-    double position{};
-    s32 fraction{};
-};
-
-/// Interpolates input signal to produce output signal.
-/// @param input The signal to interpolate.
-/// @param ratio Interpolation ratio.
-///              ratio > 1.0 results in fewer output samples.
-///              ratio < 1.0 results in more output samples.
-/// @returns Output signal.
-std::vector<s16> Interpolate(InterpolationState& state, std::vector<s16> input, double ratio);
-
-/// Interpolates input signal to produce output signal.
-/// @param input The signal to interpolate.
-/// @param input_rate The sample rate of input.
-/// @param output_rate The desired sample rate of the output.
-/// @returns Output signal.
-inline std::vector<s16> Interpolate(InterpolationState& state, std::vector<s16> input,
-                                    u32 input_rate, u32 output_rate) {
-    const double ratio = static_cast<double>(input_rate) / static_cast<double>(output_rate);
-    return Interpolate(state, std::move(input), ratio);
-}
-
-/// Nintendo Switchs DSP resampling algorithm. Based on a single channel
-void Resample(s32* output, const s32* input, s32 pitch, s32& fraction, std::size_t sample_count);
-
-} // namespace AudioCore
diff --git a/src/audio_core/audio_core.cpp b/src/audio_core/audio_core.cpp
new file mode 100644
index 0000000000..78e615a10e
--- /dev/null
+++ b/src/audio_core/audio_core.cpp
@@ -0,0 +1,68 @@
+// 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>(system)} {
+    CreateSinks();
+    // Must be created after the sinks
+    adsp = std::make_unique<AudioRenderer::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;
+}
+
+AudioRenderer::ADSP::ADSP& AudioCore::GetADSP() {
+    return *adsp;
+}
+
+void AudioCore::PauseSinks(const bool pausing) const {
+    if (pausing) {
+        output_sink->PauseStreams();
+        input_sink->PauseStreams();
+    } else {
+        output_sink->UnpauseStreams();
+        input_sink->UnpauseStreams();
+    }
+}
+
+u32 AudioCore::GetStreamQueue() const {
+    return estimated_queue.load();
+}
+
+void AudioCore::SetStreamQueue(u32 size) {
+    estimated_queue.store(size);
+}
+
+} // namespace AudioCore
diff --git a/src/audio_core/audio_core.h b/src/audio_core/audio_core.h
new file mode 100644
index 0000000000..0f7d61ee4b
--- /dev/null
+++ b/src/audio_core/audio_core.h
@@ -0,0 +1,100 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <memory>
+
+#include "audio_core/audio_manager.h"
+#include "audio_core/renderer/adsp/adsp.h"
+#include "audio_core/sink/sink.h"
+
+namespace Core {
+class System;
+}
+
+namespace AudioCore {
+
+class AudioManager;
+/**
+ * Main audio class, sotred 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.
+     */
+    AudioRenderer::ADSP::ADSP& GetADSP();
+
+    /**
+     * Pause the sink. Called from the core.
+     *
+     * @param pausing - Is this pause due to an actual pause, or shutdown?
+     *                  Unfortunately, shutdown also pauses streams, which can cause issues.
+     */
+    void PauseSinks(bool pausing) const;
+
+    /**
+     * Get the size of the current stream queue.
+     *
+     * @return Current stream queue size.
+     */
+    u32 GetStreamQueue() const;
+
+    /**
+     * Get the size of the current stream queue.
+     *
+     * @param size - New stream size.
+     */
+    void SetStreamQueue(u32 size);
+
+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<AudioRenderer::ADSP::ADSP> adsp;
+    /// Current size of the stream queue
+    std::atomic<u32> estimated_queue{0};
+};
+
+} // namespace AudioCore
diff --git a/src/audio_core/audio_event.cpp b/src/audio_core/audio_event.cpp
new file mode 100644
index 0000000000..424049c7a2
--- /dev/null
+++ b/src/audio_core/audio_event.cpp
@@ -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"
+
+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();
+    }
+    return 3;
+}
+
+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
diff --git a/src/audio_core/audio_event.h b/src/audio_core/audio_event.h
new file mode 100644
index 0000000000..82dd32dca5
--- /dev/null
+++ b/src/audio_core/audio_event.h
@@ -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 seprate 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 bythe 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
diff --git a/src/audio_core/audio_in_manager.cpp b/src/audio_core/audio_in_manager.cpp
new file mode 100644
index 0000000000..4aadb7fd61
--- /dev/null
+++ b/src/audio_core/audio_in_manager.cpp
@@ -0,0 +1,91 @@
+// 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::ERR_MAXIMUM_SESSIONS_REACHED;
+    }
+    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::vector<AudioRenderer::AudioDevice::AudioDeviceName>& names,
+                            [[maybe_unused]] const u32 max_count,
+                            [[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.size() > 1) {
+        names.push_back(AudioRenderer::AudioDevice::AudioDeviceName("Uac"));
+        return 1;
+    }
+    return 0;
+}
+
+} // namespace AudioCore::AudioIn
diff --git a/src/audio_core/audio_in_manager.h b/src/audio_core/audio_in_manager.h
new file mode 100644
index 0000000000..75b73a0b6f
--- /dev/null
+++ b/src/audio_core/audio_in_manager.h
@@ -0,0 +1,92 @@
+// 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.
+     *
+     * @oaram names     - Output container to write names to.
+     * @param max_count - Maximum numebr of deivce names to write. Unused
+     * @param filter    - Should the list be filtered? Unused.
+     * @return Number of names written.
+     */
+    u32 GetDeviceNames(std::vector<AudioRenderer::AudioDevice::AudioDeviceName>& names,
+                       u32 max_count, 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
diff --git a/src/audio_core/audio_manager.cpp b/src/audio_core/audio_manager.cpp
new file mode 100644
index 0000000000..2f1bba9c3c
--- /dev/null
+++ b/src/audio_core/audio_manager.cpp
@@ -0,0 +1,80 @@
+// 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/audio_manager.h"
+#include "audio_core/audio_out_manager.h"
+#include "core/core.h"
+
+namespace AudioCore {
+
+AudioManager::AudioManager(Core::System& system_) : system{system_} {
+    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::ERR_OPERATION_FAILED;
+    }
+
+    std::scoped_lock l{lock};
+
+    const auto index{events.GetManagerIndex(Event::Type::AudioOutManager)};
+    if (buffer_events[index] == nullptr) {
+        buffer_events[index] = buffer_func;
+        needs_update = true;
+        events.SetAudioEvent(Event::Type::AudioOutManager, true);
+    }
+    return ResultSuccess;
+}
+
+Result AudioManager::SetInManager(BufferEventFunc buffer_func) {
+    if (!running) {
+        return Service::Audio::ERR_OPERATION_FAILED;
+    }
+
+    std::scoped_lock l{lock};
+
+    const auto index{events.GetManagerIndex(Event::Type::AudioInManager)};
+    if (buffer_events[index] == nullptr) {
+        buffer_events[index] = 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) {
+        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++) {
+            if (events.CheckAudioEventSet(Event::Type(i)) || timed_out) {
+                if (buffer_events[i]) {
+                    buffer_events[i]();
+                }
+            }
+            events.SetAudioEvent(Event::Type(i), false);
+        }
+    }
+}
+
+} // namespace AudioCore
diff --git a/src/audio_core/audio_manager.h b/src/audio_core/audio_manager.h
new file mode 100644
index 0000000000..70316e9cbd
--- /dev/null
+++ b/src/audio_core/audio_manager.h
@@ -0,0 +1,101 @@
+// 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 "audio_core/audio_event.h"
+#include "core/hle/service/audio/errors.h"
+
+namespace Core {
+class System;
+}
+
+namespace AudioCore {
+
+namespace AudioOut {
+class Manager;
+}
+
+namespace AudioIn {
+class Manager;
+}
+
+/**
+ * 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(Core::System& system);
+
+    /**
+     * 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 fucntion.
+     */
+    void ThreadFunc();
+
+    /// Core system
+    Core::System& system;
+    /// Have sessions started palying?
+    bool sessions_started{};
+    /// 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
diff --git a/src/audio_core/audio_out.cpp b/src/audio_core/audio_out.cpp
deleted file mode 100644
index 83ec0221ff..0000000000
--- a/src/audio_core/audio_out.cpp
+++ /dev/null
@@ -1,62 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include "audio_core/audio_out.h"
-#include "audio_core/sink.h"
-#include "audio_core/sink_details.h"
-#include "common/assert.h"
-#include "common/logging/log.h"
-#include "common/settings.h"
-
-namespace AudioCore {
-
-/// Returns the stream format from the specified number of channels
-static Stream::Format ChannelsToStreamFormat(u32 num_channels) {
-    switch (num_channels) {
-    case 1:
-        return Stream::Format::Mono16;
-    case 2:
-        return Stream::Format::Stereo16;
-    case 6:
-        return Stream::Format::Multi51Channel16;
-    }
-
-    UNIMPLEMENTED_MSG("Unimplemented num_channels={}", num_channels);
-    return {};
-}
-
-StreamPtr AudioOut::OpenStream(Core::Timing::CoreTiming& core_timing, u32 sample_rate,
-                               u32 num_channels, std::string&& name,
-                               Stream::ReleaseCallback&& release_callback) {
-    if (!sink) {
-        sink = CreateSinkFromID(Settings::values.sink_id.GetValue(),
-                                Settings::values.audio_device_id.GetValue());
-    }
-
-    return std::make_shared<Stream>(
-        core_timing, sample_rate, ChannelsToStreamFormat(num_channels), std::move(release_callback),
-        sink->AcquireSinkStream(sample_rate, num_channels, name), std::move(name));
-}
-
-std::vector<Buffer::Tag> AudioOut::GetTagsAndReleaseBuffers(StreamPtr stream,
-                                                            std::size_t max_count) {
-    return stream->GetTagsAndReleaseBuffers(max_count);
-}
-
-std::vector<Buffer::Tag> AudioOut::GetTagsAndReleaseBuffers(StreamPtr stream) {
-    return stream->GetTagsAndReleaseBuffers();
-}
-
-void AudioOut::StartStream(StreamPtr stream) {
-    stream->Play();
-}
-
-void AudioOut::StopStream(StreamPtr stream) {
-    stream->Stop();
-}
-
-bool AudioOut::QueueBuffer(StreamPtr stream, Buffer::Tag tag, std::vector<s16>&& data) {
-    return stream->QueueBuffer(std::make_shared<Buffer>(tag, std::move(data)));
-}
-
-} // namespace AudioCore
diff --git a/src/audio_core/audio_out.h b/src/audio_core/audio_out.h
deleted file mode 100644
index 6856373f10..0000000000
--- a/src/audio_core/audio_out.h
+++ /dev/null
@@ -1,49 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include <memory>
-#include <string>
-#include <vector>
-
-#include "audio_core/buffer.h"
-#include "audio_core/sink.h"
-#include "audio_core/stream.h"
-#include "common/common_types.h"
-
-namespace Core::Timing {
-class CoreTiming;
-}
-
-namespace AudioCore {
-
-/**
- * Represents an audio playback interface, used to open and play audio streams
- */
-class AudioOut {
-public:
-    /// Opens a new audio stream
-    StreamPtr OpenStream(Core::Timing::CoreTiming& core_timing, u32 sample_rate, u32 num_channels,
-                         std::string&& name, Stream::ReleaseCallback&& release_callback);
-
-    /// Returns a vector of recently released buffers specified by tag for the specified stream
-    std::vector<Buffer::Tag> GetTagsAndReleaseBuffers(StreamPtr stream, std::size_t max_count);
-
-    /// Returns a vector of all recently released buffers specified by tag for the specified stream
-    std::vector<Buffer::Tag> GetTagsAndReleaseBuffers(StreamPtr stream);
-
-    /// Starts an audio stream for playback
-    void StartStream(StreamPtr stream);
-
-    /// Stops an audio stream that is currently playing
-    void StopStream(StreamPtr stream);
-
-    /// Queues a buffer into the specified audio stream, returns true on success
-    bool QueueBuffer(StreamPtr stream, Buffer::Tag tag, std::vector<s16>&& data);
-
-private:
-    SinkPtr sink;
-};
-
-} // namespace AudioCore
diff --git a/src/audio_core/audio_out_manager.cpp b/src/audio_core/audio_out_manager.cpp
new file mode 100644
index 0000000000..71d67de647
--- /dev/null
+++ b/src/audio_core/audio_out_manager.cpp
@@ -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::ERR_MAXIMUM_SESSIONS_REACHED;
+    }
+    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<AudioRenderer::AudioDevice::AudioDeviceName>& names) const {
+    names.push_back({"DeviceOut"});
+    return 1;
+}
+
+} // namespace AudioCore::AudioOut
diff --git a/src/audio_core/audio_out_manager.h b/src/audio_core/audio_out_manager.h
new file mode 100644
index 0000000000..24981e08f2
--- /dev/null
+++ b/src/audio_core/audio_out_manager.h
@@ -0,0 +1,89 @@
+// 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.
+     *
+     * @oaram names     - Output container to write names to.
+     * @return Number of names written.
+     */
+    u32 GetAudioOutDeviceNames(
+        std::vector<AudioRenderer::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
diff --git a/src/audio_core/audio_render_manager.cpp b/src/audio_core/audio_render_manager.cpp
new file mode 100644
index 0000000000..7a846835bb
--- /dev/null
+++ b/src/audio_core/audio_render_manager.cpp
@@ -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::AudioRenderer {
+
+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;
+}
+
+auto Manager::GetWorkBufferSize(const AudioRendererParameterInternal& params, u64& out_count)
+    -> Result {
+    if (!CheckValidRevision(params.revision)) {
+        return Service::Audio::ERR_INVALID_REVISION;
+    }
+
+    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() {
+    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::AudioRenderer
diff --git a/src/audio_core/audio_render_manager.h b/src/audio_core/audio_render_manager.h
new file mode 100644
index 0000000000..6a508ec560
--- /dev/null
+++ b/src/audio_core/audio_render_manager.h
@@ -0,0 +1,103 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+#include <memory>
+#include <mutex>
+
+#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 AudioRenderer {
+/**
+ * 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);
+
+    /**
+     * 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();
+
+    /**
+     * Add a renderer system to the manager.
+     * The system will be reguarly called to generate commands for the AudioRenderer.
+     *
+     * @param system - The system to add.
+     * @return True if the system was sucessfully 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 sucessfully 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
+    std::mutex session_lock{};
+    /// Regularly generates commands from the registered systems for the AudioRenderer
+    std::unique_ptr<SystemManager> system_manager{};
+};
+
+} // namespace AudioRenderer
+} // namespace AudioCore
diff --git a/src/audio_core/audio_renderer.cpp b/src/audio_core/audio_renderer.cpp
deleted file mode 100644
index 9191ca0931..0000000000
--- a/src/audio_core/audio_renderer.cpp
+++ /dev/null
@@ -1,343 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include <limits>
-#include <optional>
-#include <vector>
-
-#include "audio_core/audio_out.h"
-#include "audio_core/audio_renderer.h"
-#include "audio_core/common.h"
-#include "audio_core/info_updater.h"
-#include "audio_core/voice_context.h"
-#include "common/logging/log.h"
-#include "common/settings.h"
-#include "core/core_timing.h"
-#include "core/memory.h"
-
-namespace {
-[[nodiscard]] static constexpr s16 ClampToS16(s32 value) {
-    return static_cast<s16>(std::clamp(value, s32{std::numeric_limits<s16>::min()},
-                                       s32{std::numeric_limits<s16>::max()}));
-}
-
-[[nodiscard]] static constexpr s16 Mix2To1(s16 l_channel, s16 r_channel) {
-    // Mix 50% from left and 50% from right channel
-    constexpr float l_mix_amount = 50.0f / 100.0f;
-    constexpr float r_mix_amount = 50.0f / 100.0f;
-    return ClampToS16(static_cast<s32>((static_cast<float>(l_channel) * l_mix_amount) +
-                                       (static_cast<float>(r_channel) * r_mix_amount)));
-}
-
-[[maybe_unused, nodiscard]] static constexpr std::tuple<s16, s16> Mix6To2(
-    s16 fl_channel, s16 fr_channel, s16 fc_channel, [[maybe_unused]] s16 lf_channel, s16 bl_channel,
-    s16 br_channel) {
-    // Front channels are mixed 36.94%, Center channels are mixed to be 26.12% & the back channels
-    // are mixed to be 36.94%
-
-    constexpr float front_mix_amount = 36.94f / 100.0f;
-    constexpr float center_mix_amount = 26.12f / 100.0f;
-    constexpr float back_mix_amount = 36.94f / 100.0f;
-
-    // Mix 50% from left and 50% from right channel
-    const auto left = front_mix_amount * static_cast<float>(fl_channel) +
-                      center_mix_amount * static_cast<float>(fc_channel) +
-                      back_mix_amount * static_cast<float>(bl_channel);
-
-    const auto right = front_mix_amount * static_cast<float>(fr_channel) +
-                       center_mix_amount * static_cast<float>(fc_channel) +
-                       back_mix_amount * static_cast<float>(br_channel);
-
-    return {ClampToS16(static_cast<s32>(left)), ClampToS16(static_cast<s32>(right))};
-}
-
-[[nodiscard]] static constexpr std::tuple<s16, s16> Mix6To2WithCoefficients(
-    s16 fl_channel, s16 fr_channel, s16 fc_channel, s16 lf_channel, s16 bl_channel, s16 br_channel,
-    const std::array<float_le, 4>& coeff) {
-    const auto left =
-        static_cast<float>(fl_channel) * coeff[0] + static_cast<float>(fc_channel) * coeff[1] +
-        static_cast<float>(lf_channel) * coeff[2] + static_cast<float>(bl_channel) * coeff[3];
-
-    const auto right =
-        static_cast<float>(fr_channel) * coeff[0] + static_cast<float>(fc_channel) * coeff[1] +
-        static_cast<float>(lf_channel) * coeff[2] + static_cast<float>(br_channel) * coeff[3];
-
-    return {ClampToS16(static_cast<s32>(left)), ClampToS16(static_cast<s32>(right))};
-}
-
-} // namespace
-
-namespace AudioCore {
-constexpr s32 NUM_BUFFERS = 2;
-
-AudioRenderer::AudioRenderer(Core::Timing::CoreTiming& core_timing_, Core::Memory::Memory& memory_,
-                             AudioCommon::AudioRendererParameter params,
-                             Stream::ReleaseCallback&& release_callback,
-                             std::size_t instance_number)
-    : worker_params{params}, memory_pool_info(params.effect_count + params.voice_count * 4),
-      voice_context(params.voice_count), effect_context(params.effect_count), mix_context(),
-      sink_context(params.sink_count), splitter_context(),
-      voices(params.voice_count), memory{memory_},
-      command_generator(worker_params, voice_context, mix_context, splitter_context, effect_context,
-                        memory),
-      core_timing{core_timing_} {
-    behavior_info.SetUserRevision(params.revision);
-    splitter_context.Initialize(behavior_info, params.splitter_count,
-                                params.num_splitter_send_channels);
-    mix_context.Initialize(behavior_info, params.submix_count + 1, params.effect_count);
-    audio_out = std::make_unique<AudioCore::AudioOut>();
-    stream = audio_out->OpenStream(
-        core_timing, params.sample_rate, AudioCommon::STREAM_NUM_CHANNELS,
-        fmt::format("AudioRenderer-Instance{}", instance_number), std::move(release_callback));
-    process_event =
-        Core::Timing::CreateEvent(fmt::format("AudioRenderer-Instance{}-Process", instance_number),
-                                  [this](std::uintptr_t, s64, std::chrono::nanoseconds) {
-                                      ReleaseAndQueueBuffers();
-                                      return std::nullopt;
-                                  });
-    for (s32 i = 0; i < NUM_BUFFERS; ++i) {
-        QueueMixedBuffer(i);
-    }
-}
-
-AudioRenderer::~AudioRenderer() = default;
-
-Result AudioRenderer::Start() {
-    audio_out->StartStream(stream);
-    ReleaseAndQueueBuffers();
-    return ResultSuccess;
-}
-
-Result AudioRenderer::Stop() {
-    audio_out->StopStream(stream);
-    return ResultSuccess;
-}
-
-u32 AudioRenderer::GetSampleRate() const {
-    return worker_params.sample_rate;
-}
-
-u32 AudioRenderer::GetSampleCount() const {
-    return worker_params.sample_count;
-}
-
-u32 AudioRenderer::GetMixBufferCount() const {
-    return worker_params.mix_buffer_count;
-}
-
-Stream::State AudioRenderer::GetStreamState() const {
-    return stream->GetState();
-}
-
-Result AudioRenderer::UpdateAudioRenderer(const std::vector<u8>& input_params,
-                                          std::vector<u8>& output_params) {
-    std::scoped_lock lock{mutex};
-    InfoUpdater info_updater{input_params, output_params, behavior_info};
-
-    if (!info_updater.UpdateBehaviorInfo(behavior_info)) {
-        LOG_ERROR(Audio, "Failed to update behavior info input parameters");
-        return AudioCommon::Audren::ERR_INVALID_PARAMETERS;
-    }
-
-    if (!info_updater.UpdateMemoryPools(memory_pool_info)) {
-        LOG_ERROR(Audio, "Failed to update memory pool parameters");
-        return AudioCommon::Audren::ERR_INVALID_PARAMETERS;
-    }
-
-    if (!info_updater.UpdateVoiceChannelResources(voice_context)) {
-        LOG_ERROR(Audio, "Failed to update voice channel resource parameters");
-        return AudioCommon::Audren::ERR_INVALID_PARAMETERS;
-    }
-
-    if (!info_updater.UpdateVoices(voice_context, memory_pool_info, 0)) {
-        LOG_ERROR(Audio, "Failed to update voice parameters");
-        return AudioCommon::Audren::ERR_INVALID_PARAMETERS;
-    }
-
-    // TODO(ogniK): Deal with stopped audio renderer but updates still taking place
-    if (!info_updater.UpdateEffects(effect_context, true)) {
-        LOG_ERROR(Audio, "Failed to update effect parameters");
-        return AudioCommon::Audren::ERR_INVALID_PARAMETERS;
-    }
-
-    if (behavior_info.IsSplitterSupported()) {
-        if (!info_updater.UpdateSplitterInfo(splitter_context)) {
-            LOG_ERROR(Audio, "Failed to update splitter parameters");
-            return AudioCommon::Audren::ERR_INVALID_PARAMETERS;
-        }
-    }
-
-    const auto mix_result = info_updater.UpdateMixes(mix_context, worker_params.mix_buffer_count,
-                                                     splitter_context, effect_context);
-
-    if (mix_result.IsError()) {
-        LOG_ERROR(Audio, "Failed to update mix parameters");
-        return mix_result;
-    }
-
-    // TODO(ogniK): Sinks
-    if (!info_updater.UpdateSinks(sink_context)) {
-        LOG_ERROR(Audio, "Failed to update sink parameters");
-        return AudioCommon::Audren::ERR_INVALID_PARAMETERS;
-    }
-
-    // TODO(ogniK): Performance buffer
-    if (!info_updater.UpdatePerformanceBuffer()) {
-        LOG_ERROR(Audio, "Failed to update performance buffer parameters");
-        return AudioCommon::Audren::ERR_INVALID_PARAMETERS;
-    }
-
-    if (!info_updater.UpdateErrorInfo(behavior_info)) {
-        LOG_ERROR(Audio, "Failed to update error info");
-        return AudioCommon::Audren::ERR_INVALID_PARAMETERS;
-    }
-
-    if (behavior_info.IsElapsedFrameCountSupported()) {
-        if (!info_updater.UpdateRendererInfo(elapsed_frame_count)) {
-            LOG_ERROR(Audio, "Failed to update renderer info");
-            return AudioCommon::Audren::ERR_INVALID_PARAMETERS;
-        }
-    }
-    // TODO(ogniK): Statistics
-
-    if (!info_updater.WriteOutputHeader()) {
-        LOG_ERROR(Audio, "Failed to write output header");
-        return AudioCommon::Audren::ERR_INVALID_PARAMETERS;
-    }
-
-    // TODO(ogniK): Check when all sections are implemented
-
-    if (!info_updater.CheckConsumedSize()) {
-        LOG_ERROR(Audio, "Audio buffers were not consumed!");
-        return AudioCommon::Audren::ERR_INVALID_PARAMETERS;
-    }
-    return ResultSuccess;
-}
-
-void AudioRenderer::QueueMixedBuffer(Buffer::Tag tag) {
-    command_generator.PreCommand();
-    // Clear mix buffers before our next operation
-    command_generator.ClearMixBuffers();
-
-    // If the splitter is not in use, sort our mixes
-    if (!splitter_context.UsingSplitter()) {
-        mix_context.SortInfo();
-    }
-    // Sort our voices
-    voice_context.SortInfo();
-
-    // Handle samples
-    command_generator.GenerateVoiceCommands();
-    command_generator.GenerateSubMixCommands();
-    command_generator.GenerateFinalMixCommands();
-
-    command_generator.PostCommand();
-    // Base sample size
-    std::size_t BUFFER_SIZE{worker_params.sample_count};
-    // Samples, making sure to clear
-    std::vector<s16> buffer(BUFFER_SIZE * stream->GetNumChannels(), 0);
-
-    if (sink_context.InUse()) {
-        const auto stream_channel_count = stream->GetNumChannels();
-        const auto buffer_offsets = sink_context.OutputBuffers();
-        const auto channel_count = buffer_offsets.size();
-        const auto& final_mix = mix_context.GetFinalMixInfo();
-        const auto& in_params = final_mix.GetInParams();
-        std::vector<std::span<s32>> mix_buffers(channel_count);
-        for (std::size_t i = 0; i < channel_count; i++) {
-            mix_buffers[i] =
-                command_generator.GetMixBuffer(in_params.buffer_offset + buffer_offsets[i]);
-        }
-
-        for (std::size_t i = 0; i < BUFFER_SIZE; i++) {
-            if (channel_count == 1) {
-                const auto sample = ClampToS16(mix_buffers[0][i]);
-
-                // Place sample in all channels
-                for (u32 channel = 0; channel < stream_channel_count; channel++) {
-                    buffer[i * stream_channel_count + channel] = sample;
-                }
-
-                if (stream_channel_count == 6) {
-                    // Output stream has a LF channel, mute it!
-                    buffer[i * stream_channel_count + 3] = 0;
-                }
-
-            } else if (channel_count == 2) {
-                const auto l_sample = ClampToS16(mix_buffers[0][i]);
-                const auto r_sample = ClampToS16(mix_buffers[1][i]);
-                if (stream_channel_count == 1) {
-                    buffer[i * stream_channel_count + 0] = Mix2To1(l_sample, r_sample);
-                } else if (stream_channel_count == 2) {
-                    buffer[i * stream_channel_count + 0] = l_sample;
-                    buffer[i * stream_channel_count + 1] = r_sample;
-                } else if (stream_channel_count == 6) {
-                    buffer[i * stream_channel_count + 0] = l_sample;
-                    buffer[i * stream_channel_count + 1] = r_sample;
-
-                    // Combine both left and right channels to the center channel
-                    buffer[i * stream_channel_count + 2] = Mix2To1(l_sample, r_sample);
-
-                    buffer[i * stream_channel_count + 4] = l_sample;
-                    buffer[i * stream_channel_count + 5] = r_sample;
-                }
-
-            } else if (channel_count == 6) {
-                const auto fl_sample = ClampToS16(mix_buffers[0][i]);
-                const auto fr_sample = ClampToS16(mix_buffers[1][i]);
-                const auto fc_sample = ClampToS16(mix_buffers[2][i]);
-                const auto lf_sample = ClampToS16(mix_buffers[3][i]);
-                const auto bl_sample = ClampToS16(mix_buffers[4][i]);
-                const auto br_sample = ClampToS16(mix_buffers[5][i]);
-
-                if (stream_channel_count == 1) {
-                    // Games seem to ignore the center channel half the time, we use the front left
-                    // and right channel for mixing as that's where majority of the audio goes
-                    buffer[i * stream_channel_count + 0] = Mix2To1(fl_sample, fr_sample);
-                } else if (stream_channel_count == 2) {
-                    // Mix all channels into 2 channels
-                    const auto [left, right] = Mix6To2WithCoefficients(
-                        fl_sample, fr_sample, fc_sample, lf_sample, bl_sample, br_sample,
-                        sink_context.GetDownmixCoefficients());
-                    buffer[i * stream_channel_count + 0] = left;
-                    buffer[i * stream_channel_count + 1] = right;
-                } else if (stream_channel_count == 6) {
-                    // Pass through
-                    buffer[i * stream_channel_count + 0] = fl_sample;
-                    buffer[i * stream_channel_count + 1] = fr_sample;
-                    buffer[i * stream_channel_count + 2] = fc_sample;
-                    buffer[i * stream_channel_count + 3] = lf_sample;
-                    buffer[i * stream_channel_count + 4] = bl_sample;
-                    buffer[i * stream_channel_count + 5] = br_sample;
-                }
-            }
-        }
-    }
-
-    audio_out->QueueBuffer(stream, tag, std::move(buffer));
-    elapsed_frame_count++;
-    voice_context.UpdateStateByDspShared();
-}
-
-void AudioRenderer::ReleaseAndQueueBuffers() {
-    if (!stream->IsPlaying()) {
-        return;
-    }
-
-    {
-        std::scoped_lock lock{mutex};
-        const auto released_buffers{audio_out->GetTagsAndReleaseBuffers(stream)};
-        for (const auto& tag : released_buffers) {
-            QueueMixedBuffer(tag);
-        }
-    }
-
-    const f32 sample_rate = static_cast<f32>(GetSampleRate());
-    const f32 sample_count = static_cast<f32>(GetSampleCount());
-    const f32 consume_rate = sample_rate / (sample_count * (sample_count / 240));
-    const s32 ms = (1000 / static_cast<s32>(consume_rate)) - 1;
-    const std::chrono::milliseconds next_event_time(std::max(ms / NUM_BUFFERS, 1));
-    core_timing.ScheduleEvent(next_event_time, process_event, {});
-}
-
-} // namespace AudioCore
diff --git a/src/audio_core/audio_renderer.h b/src/audio_core/audio_renderer.h
deleted file mode 100644
index a67ffd592e..0000000000
--- a/src/audio_core/audio_renderer.h
+++ /dev/null
@@ -1,78 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include <array>
-#include <memory>
-#include <mutex>
-#include <vector>
-
-#include "audio_core/behavior_info.h"
-#include "audio_core/command_generator.h"
-#include "audio_core/common.h"
-#include "audio_core/effect_context.h"
-#include "audio_core/memory_pool.h"
-#include "audio_core/mix_context.h"
-#include "audio_core/sink_context.h"
-#include "audio_core/splitter_context.h"
-#include "audio_core/stream.h"
-#include "audio_core/voice_context.h"
-#include "common/common_funcs.h"
-#include "common/common_types.h"
-#include "common/swap.h"
-#include "core/hle/result.h"
-
-namespace Core::Timing {
-class CoreTiming;
-}
-
-namespace Core::Memory {
-class Memory;
-}
-
-namespace AudioCore {
-using DSPStateHolder = std::array<VoiceState*, AudioCommon::MAX_CHANNEL_COUNT>;
-
-class AudioOut;
-
-class AudioRenderer {
-public:
-    AudioRenderer(Core::Timing::CoreTiming& core_timing, Core::Memory::Memory& memory_,
-                  AudioCommon::AudioRendererParameter params,
-                  Stream::ReleaseCallback&& release_callback, std::size_t instance_number);
-    ~AudioRenderer();
-
-    [[nodiscard]] Result UpdateAudioRenderer(const std::vector<u8>& input_params,
-                                             std::vector<u8>& output_params);
-    [[nodiscard]] Result Start();
-    [[nodiscard]] Result Stop();
-    void QueueMixedBuffer(Buffer::Tag tag);
-    void ReleaseAndQueueBuffers();
-    [[nodiscard]] u32 GetSampleRate() const;
-    [[nodiscard]] u32 GetSampleCount() const;
-    [[nodiscard]] u32 GetMixBufferCount() const;
-    [[nodiscard]] Stream::State GetStreamState() const;
-
-private:
-    BehaviorInfo behavior_info{};
-
-    AudioCommon::AudioRendererParameter worker_params;
-    std::vector<ServerMemoryPoolInfo> memory_pool_info;
-    VoiceContext voice_context;
-    EffectContext effect_context;
-    MixContext mix_context;
-    SinkContext sink_context;
-    SplitterContext splitter_context;
-    std::vector<VoiceState> voices;
-    std::unique_ptr<AudioOut> audio_out;
-    StreamPtr stream;
-    Core::Memory::Memory& memory;
-    CommandGenerator command_generator;
-    std::size_t elapsed_frame_count{};
-    Core::Timing::CoreTiming& core_timing;
-    std::shared_ptr<Core::Timing::EventType> process_event;
-    std::mutex mutex;
-};
-
-} // namespace AudioCore
diff --git a/src/audio_core/behavior_info.cpp b/src/audio_core/behavior_info.cpp
deleted file mode 100644
index ea7e456179..0000000000
--- a/src/audio_core/behavior_info.cpp
+++ /dev/null
@@ -1,104 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include <cstring>
-#include "audio_core/behavior_info.h"
-#include "audio_core/common.h"
-#include "common/logging/log.h"
-
-namespace AudioCore {
-
-BehaviorInfo::BehaviorInfo() : process_revision(AudioCommon::CURRENT_PROCESS_REVISION) {}
-BehaviorInfo::~BehaviorInfo() = default;
-
-bool BehaviorInfo::UpdateOutput(std::vector<u8>& buffer, std::size_t offset) {
-    if (!AudioCommon::CanConsumeBuffer(buffer.size(), offset, sizeof(OutParams))) {
-        LOG_ERROR(Audio, "Buffer is an invalid size!");
-        return false;
-    }
-
-    OutParams params{};
-    std::memcpy(params.errors.data(), errors.data(), sizeof(ErrorInfo) * errors.size());
-    params.error_count = static_cast<u32_le>(error_count);
-    std::memcpy(buffer.data() + offset, &params, sizeof(OutParams));
-    return true;
-}
-
-void BehaviorInfo::ClearError() {
-    error_count = 0;
-}
-
-void BehaviorInfo::UpdateFlags(u64_le dest_flags) {
-    flags = dest_flags;
-}
-
-void BehaviorInfo::SetUserRevision(u32_le revision) {
-    user_revision = revision;
-}
-
-u32_le BehaviorInfo::GetUserRevision() const {
-    return user_revision;
-}
-
-u32_le BehaviorInfo::GetProcessRevision() const {
-    return process_revision;
-}
-
-bool BehaviorInfo::IsAdpcmLoopContextBugFixed() const {
-    return AudioCommon::IsRevisionSupported(2, user_revision);
-}
-
-bool BehaviorInfo::IsSplitterSupported() const {
-    return AudioCommon::IsRevisionSupported(2, user_revision);
-}
-
-bool BehaviorInfo::IsLongSizePreDelaySupported() const {
-    return AudioCommon::IsRevisionSupported(3, user_revision);
-}
-
-bool BehaviorInfo::IsAudioRendererProcessingTimeLimit80PercentSupported() const {
-    return AudioCommon::IsRevisionSupported(5, user_revision);
-}
-
-bool BehaviorInfo::IsAudioRendererProcessingTimeLimit75PercentSupported() const {
-    return AudioCommon::IsRevisionSupported(4, user_revision);
-}
-
-bool BehaviorInfo::IsAudioRendererProcessingTimeLimit70PercentSupported() const {
-    return AudioCommon::IsRevisionSupported(1, user_revision);
-}
-
-bool BehaviorInfo::IsElapsedFrameCountSupported() const {
-    return AudioCommon::IsRevisionSupported(5, user_revision);
-}
-
-bool BehaviorInfo::IsMemoryPoolForceMappingEnabled() const {
-    return (flags & 1) != 0;
-}
-
-bool BehaviorInfo::IsFlushVoiceWaveBuffersSupported() const {
-    return AudioCommon::IsRevisionSupported(5, user_revision);
-}
-
-bool BehaviorInfo::IsVoicePlayedSampleCountResetAtLoopPointSupported() const {
-    return AudioCommon::IsRevisionSupported(5, user_revision);
-}
-
-bool BehaviorInfo::IsVoicePitchAndSrcSkippedSupported() const {
-    return AudioCommon::IsRevisionSupported(5, user_revision);
-}
-
-bool BehaviorInfo::IsMixInParameterDirtyOnlyUpdateSupported() const {
-    return AudioCommon::IsRevisionSupported(7, user_revision);
-}
-
-bool BehaviorInfo::IsSplitterBugFixed() const {
-    return AudioCommon::IsRevisionSupported(5, user_revision);
-}
-
-void BehaviorInfo::CopyErrorInfo(BehaviorInfo::OutParams& dst) {
-    dst.error_count = static_cast<u32>(error_count);
-    std::copy(errors.begin(), errors.begin() + error_count, dst.errors.begin());
-}
-
-} // namespace AudioCore
diff --git a/src/audio_core/behavior_info.h b/src/audio_core/behavior_info.h
deleted file mode 100644
index b8c3159b9e..0000000000
--- a/src/audio_core/behavior_info.h
+++ /dev/null
@@ -1,71 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include <array>
-
-#include <vector>
-#include "common/common_funcs.h"
-#include "common/common_types.h"
-#include "common/swap.h"
-
-namespace AudioCore {
-class BehaviorInfo {
-public:
-    struct ErrorInfo {
-        u32_le result{};
-        INSERT_PADDING_WORDS(1);
-        u64_le result_info{};
-    };
-    static_assert(sizeof(ErrorInfo) == 0x10, "ErrorInfo is an invalid size");
-
-    struct InParams {
-        u32_le revision{};
-        u32_le padding{};
-        u64_le flags{};
-    };
-    static_assert(sizeof(InParams) == 0x10, "InParams is an invalid size");
-
-    struct OutParams {
-        std::array<ErrorInfo, 10> errors{};
-        u32_le error_count{};
-        INSERT_PADDING_BYTES(12);
-    };
-    static_assert(sizeof(OutParams) == 0xb0, "OutParams is an invalid size");
-
-    explicit BehaviorInfo();
-    ~BehaviorInfo();
-
-    bool UpdateOutput(std::vector<u8>& buffer, std::size_t offset);
-
-    void ClearError();
-    void UpdateFlags(u64_le dest_flags);
-    void SetUserRevision(u32_le revision);
-    [[nodiscard]] u32_le GetUserRevision() const;
-    [[nodiscard]] u32_le GetProcessRevision() const;
-
-    [[nodiscard]] bool IsAdpcmLoopContextBugFixed() const;
-    [[nodiscard]] bool IsSplitterSupported() const;
-    [[nodiscard]] bool IsLongSizePreDelaySupported() const;
-    [[nodiscard]] bool IsAudioRendererProcessingTimeLimit80PercentSupported() const;
-    [[nodiscard]] bool IsAudioRendererProcessingTimeLimit75PercentSupported() const;
-    [[nodiscard]] bool IsAudioRendererProcessingTimeLimit70PercentSupported() const;
-    [[nodiscard]] bool IsElapsedFrameCountSupported() const;
-    [[nodiscard]] bool IsMemoryPoolForceMappingEnabled() const;
-    [[nodiscard]] bool IsFlushVoiceWaveBuffersSupported() const;
-    [[nodiscard]] bool IsVoicePlayedSampleCountResetAtLoopPointSupported() const;
-    [[nodiscard]] bool IsVoicePitchAndSrcSkippedSupported() const;
-    [[nodiscard]] bool IsMixInParameterDirtyOnlyUpdateSupported() const;
-    [[nodiscard]] bool IsSplitterBugFixed() const;
-    void CopyErrorInfo(OutParams& dst);
-
-private:
-    u32_le process_revision{};
-    u32_le user_revision{};
-    u64_le flags{};
-    std::array<ErrorInfo, 10> errors{};
-    std::size_t error_count{};
-};
-
-} // namespace AudioCore
diff --git a/src/audio_core/buffer.h b/src/audio_core/buffer.h
deleted file mode 100644
index ac001629f9..0000000000
--- a/src/audio_core/buffer.h
+++ /dev/null
@@ -1,44 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include <memory>
-#include <vector>
-
-#include "common/common_types.h"
-
-namespace AudioCore {
-
-/**
- * Represents a buffer of audio samples to be played in an audio stream
- */
-class Buffer {
-public:
-    using Tag = u64;
-
-    Buffer(Tag tag_, std::vector<s16>&& samples_) : tag{tag_}, samples{std::move(samples_)} {}
-
-    /// Returns the raw audio data for the buffer
-    std::vector<s16>& GetSamples() {
-        return samples;
-    }
-
-    /// Returns the raw audio data for the buffer
-    const std::vector<s16>& GetSamples() const {
-        return samples;
-    }
-
-    /// Returns the buffer tag, this is provided by the game to the audout service
-    Tag GetTag() const {
-        return tag;
-    }
-
-private:
-    Tag tag;
-    std::vector<s16> samples;
-};
-
-using BufferPtr = std::shared_ptr<Buffer>;
-
-} // namespace AudioCore
diff --git a/src/audio_core/codec.cpp b/src/audio_core/codec.cpp
deleted file mode 100644
index 868b7a173d..0000000000
--- a/src/audio_core/codec.cpp
+++ /dev/null
@@ -1,77 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include <algorithm>
-
-#include "audio_core/codec.h"
-
-namespace AudioCore::Codec {
-
-std::vector<s16> DecodeADPCM(const u8* const data, std::size_t size, const ADPCM_Coeff& coeff,
-                             ADPCMState& state) {
-    // GC-ADPCM with scale factor and variable coefficients.
-    // Frames are 8 bytes long containing 14 samples each.
-    // Samples are 4 bits (one nibble) long.
-
-    constexpr std::size_t FRAME_LEN = 8;
-    constexpr std::size_t SAMPLES_PER_FRAME = 14;
-    static constexpr std::array<int, 16> SIGNED_NIBBLES{
-        0, 1, 2, 3, 4, 5, 6, 7, -8, -7, -6, -5, -4, -3, -2, -1,
-    };
-
-    const std::size_t sample_count = (size / FRAME_LEN) * SAMPLES_PER_FRAME;
-    const std::size_t ret_size =
-        sample_count % 2 == 0 ? sample_count : sample_count + 1; // Ensure multiple of two.
-    std::vector<s16> ret(ret_size);
-
-    int yn1 = state.yn1, yn2 = state.yn2;
-
-    const std::size_t NUM_FRAMES =
-        (sample_count + (SAMPLES_PER_FRAME - 1)) / SAMPLES_PER_FRAME; // Round up.
-    for (std::size_t framei = 0; framei < NUM_FRAMES; framei++) {
-        const int frame_header = data[framei * FRAME_LEN];
-        const int scale = 1 << (frame_header & 0xF);
-        const int idx = (frame_header >> 4) & 0x7;
-
-        // Coefficients are fixed point with 11 bits fractional part.
-        const int coef1 = coeff[idx * 2 + 0];
-        const int coef2 = coeff[idx * 2 + 1];
-
-        // Decodes an audio sample. One nibble produces one sample.
-        const auto decode_sample = [&](const int nibble) -> s16 {
-            const int xn = nibble * scale;
-            // We first transform everything into 11 bit fixed point, perform the second order
-            // digital filter, then transform back.
-            // 0x400 == 0.5 in 11 bit fixed point.
-            // Filter: y[n] = x[n] + 0.5 + c1 * y[n-1] + c2 * y[n-2]
-            int val = ((xn << 11) + 0x400 + coef1 * yn1 + coef2 * yn2) >> 11;
-            // Clamp to output range.
-            val = std::clamp<s32>(val, -32768, 32767);
-            // Advance output feedback.
-            yn2 = yn1;
-            yn1 = val;
-            return static_cast<s16>(val);
-        };
-
-        std::size_t outputi = framei * SAMPLES_PER_FRAME;
-        std::size_t datai = framei * FRAME_LEN + 1;
-        for (std::size_t i = 0; i < SAMPLES_PER_FRAME && outputi < sample_count; i += 2) {
-            const s16 sample1 = decode_sample(SIGNED_NIBBLES[data[datai] >> 4]);
-            ret[outputi] = sample1;
-            outputi++;
-
-            const s16 sample2 = decode_sample(SIGNED_NIBBLES[data[datai] & 0xF]);
-            ret[outputi] = sample2;
-            outputi++;
-
-            datai++;
-        }
-    }
-
-    state.yn1 = static_cast<s16>(yn1);
-    state.yn2 = static_cast<s16>(yn2);
-
-    return ret;
-}
-
-} // namespace AudioCore::Codec
diff --git a/src/audio_core/codec.h b/src/audio_core/codec.h
deleted file mode 100644
index 5a058b3684..0000000000
--- a/src/audio_core/codec.h
+++ /dev/null
@@ -1,43 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include <array>
-#include <vector>
-
-#include "common/common_types.h"
-
-namespace AudioCore::Codec {
-
-enum class PcmFormat : u32 {
-    Invalid = 0,
-    Int8 = 1,
-    Int16 = 2,
-    Int24 = 3,
-    Int32 = 4,
-    PcmFloat = 5,
-    Adpcm = 6,
-};
-
-/// See: Codec::DecodeADPCM
-struct ADPCMState {
-    // Two historical samples from previous processed buffer,
-    // required for ADPCM decoding
-    s16 yn1; ///< y[n-1]
-    s16 yn2; ///< y[n-2]
-};
-
-using ADPCM_Coeff = std::array<s16, 16>;
-
-/**
- * @param data Pointer to buffer that contains ADPCM data to decode
- * @param size Size of buffer in bytes
- * @param coeff ADPCM coefficients
- * @param state ADPCM state, this is updated with new state
- * @return Decoded stereo signed PCM16 data, sample_count in length
- */
-std::vector<s16> DecodeADPCM(const u8* data, std::size_t size, const ADPCM_Coeff& coeff,
-                             ADPCMState& state);
-
-}; // namespace AudioCore::Codec
diff --git a/src/audio_core/command_generator.cpp b/src/audio_core/command_generator.cpp
deleted file mode 100644
index f975208206..0000000000
--- a/src/audio_core/command_generator.cpp
+++ /dev/null
@@ -1,1369 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include <algorithm>
-#include <cmath>
-#include <numbers>
-
-#include "audio_core/algorithm/interpolate.h"
-#include "audio_core/command_generator.h"
-#include "audio_core/effect_context.h"
-#include "audio_core/mix_context.h"
-#include "audio_core/voice_context.h"
-#include "common/common_types.h"
-#include "core/memory.h"
-
-namespace AudioCore {
-namespace {
-constexpr std::size_t MIX_BUFFER_SIZE = 0x3f00;
-constexpr std::size_t SCALED_MIX_BUFFER_SIZE = MIX_BUFFER_SIZE << 15ULL;
-using DelayLineTimes = std::array<f32, AudioCommon::I3DL2REVERB_DELAY_LINE_COUNT>;
-
-constexpr DelayLineTimes FDN_MIN_DELAY_LINE_TIMES{5.0f, 6.0f, 13.0f, 14.0f};
-constexpr DelayLineTimes FDN_MAX_DELAY_LINE_TIMES{45.704f, 82.782f, 149.94f, 271.58f};
-constexpr DelayLineTimes DECAY0_MAX_DELAY_LINE_TIMES{17.0f, 13.0f, 9.0f, 7.0f};
-constexpr DelayLineTimes DECAY1_MAX_DELAY_LINE_TIMES{19.0f, 11.0f, 10.0f, 6.0f};
-constexpr std::array<f32, AudioCommon::I3DL2REVERB_TAPS> EARLY_TAP_TIMES{
-    0.017136f, 0.059154f, 0.161733f, 0.390186f, 0.425262f, 0.455411f, 0.689737f,
-    0.745910f, 0.833844f, 0.859502f, 0.000000f, 0.075024f, 0.168788f, 0.299901f,
-    0.337443f, 0.371903f, 0.599011f, 0.716741f, 0.817859f, 0.851664f};
-constexpr std::array<f32, AudioCommon::I3DL2REVERB_TAPS> EARLY_GAIN{
-    0.67096f, 0.61027f, 1.0f,     0.35680f, 0.68361f, 0.65978f, 0.51939f,
-    0.24712f, 0.45945f, 0.45021f, 0.64196f, 0.54879f, 0.92925f, 0.38270f,
-    0.72867f, 0.69794f, 0.5464f,  0.24563f, 0.45214f, 0.44042f};
-
-template <std::size_t N>
-void ApplyMix(std::span<s32> output, std::span<const s32> input, s32 gain, s32 sample_count) {
-    for (std::size_t i = 0; i < static_cast<std::size_t>(sample_count); i += N) {
-        for (std::size_t j = 0; j < N; j++) {
-            output[i + j] +=
-                static_cast<s32>((static_cast<s64>(input[i + j]) * gain + 0x4000) >> 15);
-        }
-    }
-}
-
-s32 ApplyMixRamp(std::span<s32> output, std::span<const s32> input, float gain, float delta,
-                 s32 sample_count) {
-    // XC2 passes in NaN mix volumes, causing further issues as we handle everything as s32 rather
-    // than float, so the NaN propogation is lost. As the samples get further modified for
-    // volume etc, they can get out of NaN range, so a later heuristic for catching this is
-    // more difficult. Handle it here by setting these samples to silence.
-    if (std::isnan(gain)) {
-        gain = 0.0f;
-        delta = 0.0f;
-    }
-
-    s32 x = 0;
-    for (s32 i = 0; i < sample_count; i++) {
-        x = static_cast<s32>(static_cast<float>(input[i]) * gain);
-        output[i] += x;
-        gain += delta;
-    }
-    return x;
-}
-
-void ApplyGain(std::span<s32> output, std::span<const s32> input, s32 gain, s32 delta,
-               s32 sample_count) {
-    for (s32 i = 0; i < sample_count; i++) {
-        output[i] = static_cast<s32>((static_cast<s64>(input[i]) * gain + 0x4000) >> 15);
-        gain += delta;
-    }
-}
-
-void ApplyGainWithoutDelta(std::span<s32> output, std::span<const s32> input, s32 gain,
-                           s32 sample_count) {
-    for (s32 i = 0; i < sample_count; i++) {
-        output[i] = static_cast<s32>((static_cast<s64>(input[i]) * gain + 0x4000) >> 15);
-    }
-}
-
-s32 ApplyMixDepop(std::span<s32> output, s32 first_sample, s32 delta, s32 sample_count) {
-    const bool positive = first_sample > 0;
-    auto final_sample = std::abs(first_sample);
-    for (s32 i = 0; i < sample_count; i++) {
-        final_sample = static_cast<s32>((static_cast<s64>(final_sample) * delta) >> 15);
-        if (positive) {
-            output[i] += final_sample;
-        } else {
-            output[i] -= final_sample;
-        }
-    }
-    if (positive) {
-        return final_sample;
-    } else {
-        return -final_sample;
-    }
-}
-
-float Pow10(float x) {
-    if (x >= 0.0f) {
-        return 1.0f;
-    } else if (x <= -5.3f) {
-        return 0.0f;
-    }
-    return std::pow(10.0f, x);
-}
-
-float SinD(float degrees) {
-    return std::sin(degrees * std::numbers::pi_v<float> / 180.0f);
-}
-
-float CosD(float degrees) {
-    return std::cos(degrees * std::numbers::pi_v<float> / 180.0f);
-}
-
-float ToFloat(s32 sample) {
-    return static_cast<float>(sample) / 65536.f;
-}
-
-s32 ToS32(float sample) {
-    constexpr auto min = -8388608.0f;
-    constexpr auto max = 8388607.f;
-    float rescaled_sample = sample * 65536.0f;
-    if (rescaled_sample < min) {
-        rescaled_sample = min;
-    }
-    if (rescaled_sample > max) {
-        rescaled_sample = max;
-    }
-    return static_cast<s32>(rescaled_sample);
-}
-
-constexpr std::array<u8, 20> REVERB_TAP_INDEX_1CH{0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
-                                                  0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
-
-constexpr std::array<u8, 20> REVERB_TAP_INDEX_2CH{0, 0, 0, 1, 1, 1, 1, 0, 0, 0,
-                                                  1, 1, 1, 0, 0, 0, 0, 1, 1, 1};
-
-constexpr std::array<u8, 20> REVERB_TAP_INDEX_4CH{0, 0, 0, 1, 1, 1, 1, 2, 2, 2,
-                                                  1, 1, 1, 0, 0, 0, 0, 3, 3, 3};
-
-constexpr std::array<u8, 20> REVERB_TAP_INDEX_6CH{4, 0, 0, 1, 1, 1, 1, 2, 2, 2,
-                                                  1, 1, 1, 0, 0, 0, 0, 3, 3, 3};
-
-template <std::size_t CHANNEL_COUNT>
-void ApplyReverbGeneric(
-    I3dl2ReverbState& state,
-    const std::array<std::span<const s32>, AudioCommon::MAX_CHANNEL_COUNT>& input,
-    const std::array<std::span<s32>, AudioCommon::MAX_CHANNEL_COUNT>& output, s32 sample_count) {
-
-    auto GetTapLookup = []() {
-        if constexpr (CHANNEL_COUNT == 1) {
-            return REVERB_TAP_INDEX_1CH;
-        } else if constexpr (CHANNEL_COUNT == 2) {
-            return REVERB_TAP_INDEX_2CH;
-        } else if constexpr (CHANNEL_COUNT == 4) {
-            return REVERB_TAP_INDEX_4CH;
-        } else if constexpr (CHANNEL_COUNT == 6) {
-            return REVERB_TAP_INDEX_6CH;
-        }
-    };
-
-    const auto& tap_index_lut = GetTapLookup();
-    for (s32 sample = 0; sample < sample_count; sample++) {
-        std::array<f32, CHANNEL_COUNT> out_samples{};
-        std::array<f32, AudioCommon::I3DL2REVERB_DELAY_LINE_COUNT> fsamp{};
-        std::array<f32, AudioCommon::I3DL2REVERB_DELAY_LINE_COUNT> mixed{};
-        std::array<f32, AudioCommon::I3DL2REVERB_DELAY_LINE_COUNT> osamp{};
-
-        // Mix everything into a single sample
-        s32 temp_mixed_sample = 0;
-        for (std::size_t i = 0; i < CHANNEL_COUNT; i++) {
-            temp_mixed_sample += input[i][sample];
-        }
-        const auto current_sample = ToFloat(temp_mixed_sample);
-        const auto early_tap = state.early_delay_line.TapOut(state.early_to_late_taps);
-
-        for (std::size_t i = 0; i < AudioCommon::I3DL2REVERB_TAPS; i++) {
-            const auto tapped_samp =
-                state.early_delay_line.TapOut(state.early_tap_steps[i]) * EARLY_GAIN[i];
-            out_samples[tap_index_lut[i]] += tapped_samp;
-
-            if constexpr (CHANNEL_COUNT == 6) {
-                // handle lfe
-                out_samples[5] += tapped_samp;
-            }
-        }
-
-        state.lowpass_0 = current_sample * state.lowpass_2 + state.lowpass_0 * state.lowpass_1;
-        state.early_delay_line.Tick(state.lowpass_0);
-
-        for (std::size_t i = 0; i < CHANNEL_COUNT; i++) {
-            out_samples[i] *= state.early_gain;
-        }
-
-        // Two channel seems to apply a latet gain, we require to save this
-        f32 filter{};
-        for (std::size_t i = 0; i < AudioCommon::I3DL2REVERB_DELAY_LINE_COUNT; i++) {
-            filter = state.fdn_delay_line[i].GetOutputSample();
-            const auto computed = filter * state.lpf_coefficients[0][i] + state.shelf_filter[i];
-            state.shelf_filter[i] =
-                filter * state.lpf_coefficients[1][i] + computed * state.lpf_coefficients[2][i];
-            fsamp[i] = computed;
-        }
-
-        // Mixing matrix
-        mixed[0] = fsamp[1] + fsamp[2];
-        mixed[1] = -fsamp[0] - fsamp[3];
-        mixed[2] = fsamp[0] - fsamp[3];
-        mixed[3] = fsamp[1] - fsamp[2];
-
-        if constexpr (CHANNEL_COUNT == 2) {
-            for (auto& mix : mixed) {
-                mix *= (filter * state.late_gain);
-            }
-        }
-
-        for (std::size_t i = 0; i < AudioCommon::I3DL2REVERB_DELAY_LINE_COUNT; i++) {
-            const auto late = early_tap * state.late_gain;
-            osamp[i] = state.decay_delay_line0[i].Tick(late + mixed[i]);
-            osamp[i] = state.decay_delay_line1[i].Tick(osamp[i]);
-            state.fdn_delay_line[i].Tick(osamp[i]);
-        }
-
-        if constexpr (CHANNEL_COUNT == 1) {
-            output[0][sample] = ToS32(state.dry_gain * ToFloat(input[0][sample]) +
-                                      (out_samples[0] + osamp[0] + osamp[1]));
-        } else if constexpr (CHANNEL_COUNT == 2 || CHANNEL_COUNT == 4) {
-            for (std::size_t i = 0; i < CHANNEL_COUNT; i++) {
-                output[i][sample] =
-                    ToS32(state.dry_gain * ToFloat(input[i][sample]) + (out_samples[i] + osamp[i]));
-            }
-        } else if constexpr (CHANNEL_COUNT == 6) {
-            const auto temp_center = state.center_delay_line.Tick(0.5f * (osamp[2] - osamp[3]));
-            for (std::size_t i = 0; i < 4; i++) {
-                output[i][sample] =
-                    ToS32(state.dry_gain * ToFloat(input[i][sample]) + (out_samples[i] + osamp[i]));
-            }
-            output[4][sample] =
-                ToS32(state.dry_gain * ToFloat(input[4][sample]) + (out_samples[4] + temp_center));
-            output[5][sample] =
-                ToS32(state.dry_gain * ToFloat(input[5][sample]) + (out_samples[5] + osamp[3]));
-        }
-    }
-}
-
-} // namespace
-
-CommandGenerator::CommandGenerator(AudioCommon::AudioRendererParameter& worker_params_,
-                                   VoiceContext& voice_context_, MixContext& mix_context_,
-                                   SplitterContext& splitter_context_,
-                                   EffectContext& effect_context_, Core::Memory::Memory& memory_)
-    : worker_params(worker_params_), voice_context(voice_context_), mix_context(mix_context_),
-      splitter_context(splitter_context_), effect_context(effect_context_), memory(memory_),
-      mix_buffer((worker_params.mix_buffer_count + AudioCommon::MAX_CHANNEL_COUNT) *
-                 worker_params.sample_count),
-      sample_buffer(MIX_BUFFER_SIZE),
-      depop_buffer((worker_params.mix_buffer_count + AudioCommon::MAX_CHANNEL_COUNT) *
-                   worker_params.sample_count) {}
-CommandGenerator::~CommandGenerator() = default;
-
-void CommandGenerator::ClearMixBuffers() {
-    std::fill(mix_buffer.begin(), mix_buffer.end(), 0);
-    std::fill(sample_buffer.begin(), sample_buffer.end(), 0);
-    // std::fill(depop_buffer.begin(), depop_buffer.end(), 0);
-}
-
-void CommandGenerator::GenerateVoiceCommands() {
-    if (dumping_frame) {
-        LOG_DEBUG(Audio, "(DSP_TRACE) GenerateVoiceCommands");
-    }
-    // Grab all our voices
-    const auto voice_count = voice_context.GetVoiceCount();
-    for (std::size_t i = 0; i < voice_count; i++) {
-        auto& voice_info = voice_context.GetSortedInfo(i);
-        // Update voices and check if we should queue them
-        if (voice_info.ShouldSkip() || !voice_info.UpdateForCommandGeneration(voice_context)) {
-            continue;
-        }
-
-        // Queue our voice
-        GenerateVoiceCommand(voice_info);
-    }
-    // Update our splitters
-    splitter_context.UpdateInternalState();
-}
-
-void CommandGenerator::GenerateVoiceCommand(ServerVoiceInfo& voice_info) {
-    auto& in_params = voice_info.GetInParams();
-    const auto channel_count = in_params.channel_count;
-
-    for (s32 channel = 0; channel < channel_count; channel++) {
-        const auto resource_id = in_params.voice_channel_resource_id[channel];
-        auto& dsp_state = voice_context.GetDspSharedState(resource_id);
-        auto& channel_resource = voice_context.GetChannelResource(resource_id);
-
-        // Decode our samples for our channel
-        GenerateDataSourceCommand(voice_info, dsp_state, channel);
-
-        if (in_params.should_depop) {
-            in_params.last_volume = 0.0f;
-        } else if (in_params.splitter_info_id != AudioCommon::NO_SPLITTER ||
-                   in_params.mix_id != AudioCommon::NO_MIX) {
-            // Apply a biquad filter if needed
-            GenerateBiquadFilterCommandForVoice(voice_info, dsp_state,
-                                                worker_params.mix_buffer_count, channel);
-            // Base voice volume ramping
-            GenerateVolumeRampCommand(in_params.last_volume, in_params.volume, channel,
-                                      in_params.node_id);
-            in_params.last_volume = in_params.volume;
-
-            if (in_params.mix_id != AudioCommon::NO_MIX) {
-                // If we're using a mix id
-                auto& mix_info = mix_context.GetInfo(in_params.mix_id);
-                const auto& dest_mix_params = mix_info.GetInParams();
-
-                // Voice Mixing
-                GenerateVoiceMixCommand(
-                    channel_resource.GetCurrentMixVolume(), channel_resource.GetLastMixVolume(),
-                    dsp_state, dest_mix_params.buffer_offset, dest_mix_params.buffer_count,
-                    worker_params.mix_buffer_count + channel, in_params.node_id);
-
-                // Update last mix volumes
-                channel_resource.UpdateLastMixVolumes();
-            } else if (in_params.splitter_info_id != AudioCommon::NO_SPLITTER) {
-                s32 base = channel;
-                while (auto* destination_data =
-                           GetDestinationData(in_params.splitter_info_id, base)) {
-                    base += channel_count;
-
-                    if (!destination_data->IsConfigured()) {
-                        continue;
-                    }
-                    if (destination_data->GetMixId() >= static_cast<int>(mix_context.GetCount())) {
-                        continue;
-                    }
-
-                    const auto& mix_info = mix_context.GetInfo(destination_data->GetMixId());
-                    const auto& dest_mix_params = mix_info.GetInParams();
-                    GenerateVoiceMixCommand(
-                        destination_data->CurrentMixVolumes(), destination_data->LastMixVolumes(),
-                        dsp_state, dest_mix_params.buffer_offset, dest_mix_params.buffer_count,
-                        worker_params.mix_buffer_count + channel, in_params.node_id);
-                    destination_data->MarkDirty();
-                }
-            }
-            // Update biquad filter enabled states
-            for (std::size_t i = 0; i < AudioCommon::MAX_BIQUAD_FILTERS; i++) {
-                in_params.was_biquad_filter_enabled[i] = in_params.biquad_filter[i].enabled;
-            }
-        }
-    }
-}
-
-void CommandGenerator::GenerateSubMixCommands() {
-    const auto mix_count = mix_context.GetCount();
-    for (std::size_t i = 0; i < mix_count; i++) {
-        auto& mix_info = mix_context.GetSortedInfo(i);
-        const auto& in_params = mix_info.GetInParams();
-        if (!in_params.in_use || in_params.mix_id == AudioCommon::FINAL_MIX) {
-            continue;
-        }
-        GenerateSubMixCommand(mix_info);
-    }
-}
-
-void CommandGenerator::GenerateFinalMixCommands() {
-    GenerateFinalMixCommand();
-}
-
-void CommandGenerator::PreCommand() {
-    if (!dumping_frame) {
-        return;
-    }
-    for (std::size_t i = 0; i < splitter_context.GetInfoCount(); i++) {
-        const auto& base = splitter_context.GetInfo(i);
-        std::string graph = fmt::format("b[{}]", i);
-        const auto* head = base.GetHead();
-        while (head != nullptr) {
-            graph += fmt::format("->{}", head->GetMixId());
-            head = head->GetNextDestination();
-        }
-        LOG_DEBUG(Audio, "(DSP_TRACE) SplitterGraph splitter_info={}, {}", i, graph);
-    }
-}
-
-void CommandGenerator::PostCommand() {
-    if (!dumping_frame) {
-        return;
-    }
-    dumping_frame = false;
-}
-
-void CommandGenerator::GenerateDataSourceCommand(ServerVoiceInfo& voice_info, VoiceState& dsp_state,
-                                                 s32 channel) {
-    const auto& in_params = voice_info.GetInParams();
-    const auto depop = in_params.should_depop;
-
-    if (depop) {
-        if (in_params.mix_id != AudioCommon::NO_MIX) {
-            auto& mix_info = mix_context.GetInfo(in_params.mix_id);
-            const auto& mix_in = mix_info.GetInParams();
-            GenerateDepopPrepareCommand(dsp_state, mix_in.buffer_count, mix_in.buffer_offset);
-        } else if (in_params.splitter_info_id != AudioCommon::NO_SPLITTER) {
-            s32 index{};
-            while (const auto* destination =
-                       GetDestinationData(in_params.splitter_info_id, index++)) {
-                if (!destination->IsConfigured()) {
-                    continue;
-                }
-                auto& mix_info = mix_context.GetInfo(destination->GetMixId());
-                const auto& mix_in = mix_info.GetInParams();
-                GenerateDepopPrepareCommand(dsp_state, mix_in.buffer_count, mix_in.buffer_offset);
-            }
-        }
-    } else {
-        switch (in_params.sample_format) {
-        case SampleFormat::Pcm8:
-        case SampleFormat::Pcm16:
-        case SampleFormat::Pcm32:
-        case SampleFormat::PcmFloat:
-            DecodeFromWaveBuffers(voice_info, GetChannelMixBuffer(channel), dsp_state, channel,
-                                  worker_params.sample_rate, worker_params.sample_count,
-                                  in_params.node_id);
-            break;
-        case SampleFormat::Adpcm:
-            ASSERT(channel == 0 && in_params.channel_count == 1);
-            DecodeFromWaveBuffers(voice_info, GetChannelMixBuffer(0), dsp_state, 0,
-                                  worker_params.sample_rate, worker_params.sample_count,
-                                  in_params.node_id);
-            break;
-        default:
-            ASSERT_MSG(false, "Unimplemented sample format={}", in_params.sample_format);
-        }
-    }
-}
-
-void CommandGenerator::GenerateBiquadFilterCommandForVoice(ServerVoiceInfo& voice_info,
-                                                           VoiceState& dsp_state,
-                                                           [[maybe_unused]] s32 mix_buffer_count,
-                                                           [[maybe_unused]] s32 channel) {
-    for (std::size_t i = 0; i < AudioCommon::MAX_BIQUAD_FILTERS; i++) {
-        const auto& in_params = voice_info.GetInParams();
-        auto& biquad_filter = in_params.biquad_filter[i];
-        // Check if biquad filter is actually used
-        if (!biquad_filter.enabled) {
-            continue;
-        }
-
-        // Reinitialize our biquad filter state if it was enabled previously
-        if (!in_params.was_biquad_filter_enabled[i]) {
-            dsp_state.biquad_filter_state.fill(0);
-        }
-
-        // Generate biquad filter
-        // GenerateBiquadFilterCommand(mix_buffer_count, biquad_filter,
-        // dsp_state.biquad_filter_state,
-        //                            mix_buffer_count + channel, mix_buffer_count + channel,
-        //                            worker_params.sample_count, voice_info.GetInParams().node_id);
-    }
-}
-
-void CommandGenerator::GenerateBiquadFilterCommand([[maybe_unused]] s32 mix_buffer_id,
-                                                   const BiquadFilterParameter& params,
-                                                   std::array<s64, 2>& state,
-                                                   std::size_t input_offset,
-                                                   std::size_t output_offset, s32 sample_count,
-                                                   s32 node_id) {
-    if (dumping_frame) {
-        LOG_DEBUG(Audio,
-                  "(DSP_TRACE) GenerateBiquadFilterCommand node_id={}, "
-                  "input_mix_buffer={}, output_mix_buffer={}",
-                  node_id, input_offset, output_offset);
-    }
-    std::span<const s32> input = GetMixBuffer(input_offset);
-    std::span<s32> output = GetMixBuffer(output_offset);
-
-    // Biquad filter parameters
-    const auto [n0, n1, n2] = params.numerator;
-    const auto [d0, d1] = params.denominator;
-
-    // Biquad filter states
-    auto [s0, s1] = state;
-
-    constexpr s64 int32_min = std::numeric_limits<s32>::min();
-    constexpr s64 int32_max = std::numeric_limits<s32>::max();
-
-    for (int i = 0; i < sample_count; ++i) {
-        const auto sample = static_cast<s64>(input[i]);
-        const auto f = (sample * n0 + s0 + 0x4000) >> 15;
-        const auto y = std::clamp(f, int32_min, int32_max);
-        s0 = sample * n1 + y * d0 + s1;
-        s1 = sample * n2 + y * d1;
-        output[i] = static_cast<s32>(y);
-    }
-
-    state = {s0, s1};
-}
-
-void CommandGenerator::GenerateDepopPrepareCommand(VoiceState& dsp_state,
-                                                   std::size_t mix_buffer_count,
-                                                   std::size_t mix_buffer_offset) {
-    for (std::size_t i = 0; i < mix_buffer_count; i++) {
-        auto& sample = dsp_state.previous_samples[i];
-        if (sample != 0) {
-            depop_buffer[mix_buffer_offset + i] += sample;
-            sample = 0;
-        }
-    }
-}
-
-void CommandGenerator::GenerateDepopForMixBuffersCommand(std::size_t mix_buffer_count,
-                                                         std::size_t mix_buffer_offset,
-                                                         s32 sample_rate) {
-    const std::size_t end_offset =
-        std::min(mix_buffer_offset + mix_buffer_count, GetTotalMixBufferCount());
-    const s32 delta = sample_rate == 48000 ? 0x7B29 : 0x78CB;
-    for (std::size_t i = mix_buffer_offset; i < end_offset; i++) {
-        if (depop_buffer[i] == 0) {
-            continue;
-        }
-
-        depop_buffer[i] =
-            ApplyMixDepop(GetMixBuffer(i), depop_buffer[i], delta, worker_params.sample_count);
-    }
-}
-
-void CommandGenerator::GenerateEffectCommand(ServerMixInfo& mix_info) {
-    const std::size_t effect_count = effect_context.GetCount();
-    const auto buffer_offset = mix_info.GetInParams().buffer_offset;
-    for (std::size_t i = 0; i < effect_count; i++) {
-        const auto index = mix_info.GetEffectOrder(i);
-        if (index == AudioCommon::NO_EFFECT_ORDER) {
-            break;
-        }
-        auto* info = effect_context.GetInfo(index);
-        const auto type = info->GetType();
-
-        // TODO(ogniK): Finish remaining effects
-        switch (type) {
-        case EffectType::Aux:
-            GenerateAuxCommand(buffer_offset, info, info->IsEnabled());
-            break;
-        case EffectType::I3dl2Reverb:
-            GenerateI3dl2ReverbEffectCommand(buffer_offset, info, info->IsEnabled());
-            break;
-        case EffectType::BiquadFilter:
-            GenerateBiquadFilterEffectCommand(buffer_offset, info, info->IsEnabled());
-            break;
-        default:
-            break;
-        }
-
-        info->UpdateForCommandGeneration();
-    }
-}
-
-void CommandGenerator::GenerateI3dl2ReverbEffectCommand(s32 mix_buffer_offset, EffectBase* info,
-                                                        bool enabled) {
-    auto* reverb = dynamic_cast<EffectI3dl2Reverb*>(info);
-    const auto& params = reverb->GetParams();
-    auto& state = reverb->GetState();
-    const auto channel_count = params.channel_count;
-
-    if (channel_count != 1 && channel_count != 2 && channel_count != 4 && channel_count != 6) {
-        return;
-    }
-
-    std::array<std::span<const s32>, AudioCommon::MAX_CHANNEL_COUNT> input{};
-    std::array<std::span<s32>, AudioCommon::MAX_CHANNEL_COUNT> output{};
-
-    const auto status = params.status;
-    for (s32 i = 0; i < channel_count; i++) {
-        input[i] = GetMixBuffer(mix_buffer_offset + params.input[i]);
-        output[i] = GetMixBuffer(mix_buffer_offset + params.output[i]);
-    }
-
-    if (enabled) {
-        if (status == ParameterStatus::Initialized) {
-            InitializeI3dl2Reverb(reverb->GetParams(), state, info->GetWorkBuffer());
-        } else if (status == ParameterStatus::Updating) {
-            UpdateI3dl2Reverb(reverb->GetParams(), state, false);
-        }
-    }
-
-    if (enabled) {
-        switch (channel_count) {
-        case 1:
-            ApplyReverbGeneric<1>(state, input, output, worker_params.sample_count);
-            break;
-        case 2:
-            ApplyReverbGeneric<2>(state, input, output, worker_params.sample_count);
-            break;
-        case 4:
-            ApplyReverbGeneric<4>(state, input, output, worker_params.sample_count);
-            break;
-        case 6:
-            ApplyReverbGeneric<6>(state, input, output, worker_params.sample_count);
-            break;
-        }
-    } else {
-        for (s32 i = 0; i < channel_count; i++) {
-            // Only copy if the buffer input and output do not match!
-            if ((mix_buffer_offset + params.input[i]) != (mix_buffer_offset + params.output[i])) {
-                std::memcpy(output[i].data(), input[i].data(),
-                            worker_params.sample_count * sizeof(s32));
-            }
-        }
-    }
-}
-
-void CommandGenerator::GenerateBiquadFilterEffectCommand(s32 mix_buffer_offset, EffectBase* info,
-                                                         bool enabled) {
-    if (!enabled) {
-        return;
-    }
-    const auto& params = dynamic_cast<EffectBiquadFilter*>(info)->GetParams();
-    const auto channel_count = params.channel_count;
-    for (s32 i = 0; i < channel_count; i++) {
-        // TODO(ogniK): Actually implement biquad filter
-        if (params.input[i] != params.output[i]) {
-            std::span<const s32> input = GetMixBuffer(mix_buffer_offset + params.input[i]);
-            std::span<s32> output = GetMixBuffer(mix_buffer_offset + params.output[i]);
-            ApplyMix<1>(output, input, 32768, worker_params.sample_count);
-        }
-    }
-}
-
-void CommandGenerator::GenerateAuxCommand(s32 mix_buffer_offset, EffectBase* info, bool enabled) {
-    auto* aux = dynamic_cast<EffectAuxInfo*>(info);
-    const auto& params = aux->GetParams();
-    if (aux->GetSendBuffer() != 0 && aux->GetRecvBuffer() != 0) {
-        const auto max_channels = params.count;
-        u32 offset{};
-        for (u32 channel = 0; channel < max_channels; channel++) {
-            u32 write_count = 0;
-            if (channel == (max_channels - 1)) {
-                write_count = offset + worker_params.sample_count;
-            }
-
-            const auto input_index = params.input_mix_buffers[channel] + mix_buffer_offset;
-            const auto output_index = params.output_mix_buffers[channel] + mix_buffer_offset;
-
-            if (enabled) {
-                AuxInfoDSP send_info{};
-                AuxInfoDSP recv_info{};
-                memory.ReadBlock(aux->GetSendInfo(), &send_info, sizeof(AuxInfoDSP));
-                memory.ReadBlock(aux->GetRecvInfo(), &recv_info, sizeof(AuxInfoDSP));
-
-                WriteAuxBuffer(send_info, aux->GetSendBuffer(), params.sample_count,
-                               GetMixBuffer(input_index), worker_params.sample_count, offset,
-                               write_count);
-                memory.WriteBlock(aux->GetSendInfo(), &send_info, sizeof(AuxInfoDSP));
-
-                const auto samples_read = ReadAuxBuffer(
-                    recv_info, aux->GetRecvBuffer(), params.sample_count,
-                    GetMixBuffer(output_index), worker_params.sample_count, offset, write_count);
-                memory.WriteBlock(aux->GetRecvInfo(), &recv_info, sizeof(AuxInfoDSP));
-
-                if (samples_read != static_cast<int>(worker_params.sample_count) &&
-                    samples_read <= params.sample_count) {
-                    std::memset(GetMixBuffer(output_index).data(), 0,
-                                params.sample_count - samples_read);
-                }
-            } else {
-                AuxInfoDSP empty{};
-                memory.WriteBlock(aux->GetSendInfo(), &empty, sizeof(AuxInfoDSP));
-                memory.WriteBlock(aux->GetRecvInfo(), &empty, sizeof(AuxInfoDSP));
-                if (output_index != input_index) {
-                    std::memcpy(GetMixBuffer(output_index).data(), GetMixBuffer(input_index).data(),
-                                worker_params.sample_count * sizeof(s32));
-                }
-            }
-
-            offset += worker_params.sample_count;
-        }
-    }
-}
-
-ServerSplitterDestinationData* CommandGenerator::GetDestinationData(s32 splitter_id, s32 index) {
-    if (splitter_id == AudioCommon::NO_SPLITTER) {
-        return nullptr;
-    }
-    return splitter_context.GetDestinationData(splitter_id, index);
-}
-
-s32 CommandGenerator::WriteAuxBuffer(AuxInfoDSP& dsp_info, VAddr send_buffer, u32 max_samples,
-                                     std::span<const s32> data, u32 sample_count, u32 write_offset,
-                                     u32 write_count) {
-    if (max_samples == 0) {
-        return 0;
-    }
-    u32 offset = dsp_info.write_offset + write_offset;
-    if (send_buffer == 0 || offset > max_samples) {
-        return 0;
-    }
-
-    s32 data_offset{};
-    u32 remaining = sample_count;
-    while (remaining > 0) {
-        // Get position in buffer
-        const auto base = send_buffer + (offset * sizeof(u32));
-        const auto samples_to_grab = std::min(max_samples - offset, remaining);
-        // Write to output
-        memory.WriteBlock(base, (data.data() + data_offset), samples_to_grab * sizeof(u32));
-        offset = (offset + samples_to_grab) % max_samples;
-        remaining -= samples_to_grab;
-        data_offset += samples_to_grab;
-    }
-
-    if (write_count != 0) {
-        dsp_info.write_offset = (dsp_info.write_offset + write_count) % max_samples;
-    }
-    return sample_count;
-}
-
-s32 CommandGenerator::ReadAuxBuffer(AuxInfoDSP& recv_info, VAddr recv_buffer, u32 max_samples,
-                                    std::span<s32> out_data, u32 sample_count, u32 read_offset,
-                                    u32 read_count) {
-    if (max_samples == 0) {
-        return 0;
-    }
-
-    u32 offset = recv_info.read_offset + read_offset;
-    if (recv_buffer == 0 || offset > max_samples) {
-        return 0;
-    }
-
-    u32 remaining = sample_count;
-    s32 data_offset{};
-    while (remaining > 0) {
-        const auto base = recv_buffer + (offset * sizeof(u32));
-        const auto samples_to_grab = std::min(max_samples - offset, remaining);
-        std::vector<s32> buffer(samples_to_grab);
-        memory.ReadBlock(base, buffer.data(), buffer.size() * sizeof(u32));
-        std::memcpy(out_data.data() + data_offset, buffer.data(), buffer.size() * sizeof(u32));
-        offset = (offset + samples_to_grab) % max_samples;
-        remaining -= samples_to_grab;
-        data_offset += samples_to_grab;
-    }
-
-    if (read_count != 0) {
-        recv_info.read_offset = (recv_info.read_offset + read_count) % max_samples;
-    }
-    return sample_count;
-}
-
-void CommandGenerator::InitializeI3dl2Reverb(I3dl2ReverbParams& info, I3dl2ReverbState& state,
-                                             std::vector<u8>& work_buffer) {
-    // Reset state
-    state.lowpass_0 = 0.0f;
-    state.lowpass_1 = 0.0f;
-    state.lowpass_2 = 0.0f;
-
-    state.early_delay_line.Reset();
-    state.early_tap_steps.fill(0);
-    state.early_gain = 0.0f;
-    state.late_gain = 0.0f;
-    state.early_to_late_taps = 0;
-    for (std::size_t i = 0; i < AudioCommon::I3DL2REVERB_DELAY_LINE_COUNT; i++) {
-        state.fdn_delay_line[i].Reset();
-        state.decay_delay_line0[i].Reset();
-        state.decay_delay_line1[i].Reset();
-    }
-    state.last_reverb_echo = 0.0f;
-    state.center_delay_line.Reset();
-    for (auto& coef : state.lpf_coefficients) {
-        coef.fill(0.0f);
-    }
-    state.shelf_filter.fill(0.0f);
-    state.dry_gain = 0.0f;
-
-    const auto sample_rate = info.sample_rate / 1000;
-    f32* work_buffer_ptr = reinterpret_cast<f32*>(work_buffer.data());
-
-    s32 delay_samples{};
-    for (std::size_t i = 0; i < AudioCommon::I3DL2REVERB_DELAY_LINE_COUNT; i++) {
-        delay_samples =
-            AudioCommon::CalculateDelaySamples(sample_rate, FDN_MAX_DELAY_LINE_TIMES[i]);
-        state.fdn_delay_line[i].Initialize(delay_samples, work_buffer_ptr);
-        work_buffer_ptr += delay_samples + 1;
-
-        delay_samples =
-            AudioCommon::CalculateDelaySamples(sample_rate, DECAY0_MAX_DELAY_LINE_TIMES[i]);
-        state.decay_delay_line0[i].Initialize(delay_samples, 0.0f, work_buffer_ptr);
-        work_buffer_ptr += delay_samples + 1;
-
-        delay_samples =
-            AudioCommon::CalculateDelaySamples(sample_rate, DECAY1_MAX_DELAY_LINE_TIMES[i]);
-        state.decay_delay_line1[i].Initialize(delay_samples, 0.0f, work_buffer_ptr);
-        work_buffer_ptr += delay_samples + 1;
-    }
-    delay_samples = AudioCommon::CalculateDelaySamples(sample_rate, 5.0f);
-    state.center_delay_line.Initialize(delay_samples, work_buffer_ptr);
-    work_buffer_ptr += delay_samples + 1;
-
-    delay_samples = AudioCommon::CalculateDelaySamples(sample_rate, 400.0f);
-    state.early_delay_line.Initialize(delay_samples, work_buffer_ptr);
-
-    UpdateI3dl2Reverb(info, state, true);
-}
-
-void CommandGenerator::UpdateI3dl2Reverb(I3dl2ReverbParams& info, I3dl2ReverbState& state,
-                                         bool should_clear) {
-
-    state.dry_gain = info.dry_gain;
-    state.shelf_filter.fill(0.0f);
-    state.lowpass_0 = 0.0f;
-    state.early_gain = Pow10(std::min(info.room + info.reflection, 5000.0f) / 2000.0f);
-    state.late_gain = Pow10(std::min(info.room + info.reverb, 5000.0f) / 2000.0f);
-
-    const auto sample_rate = info.sample_rate / 1000;
-    const f32 hf_gain = Pow10(info.room_hf / 2000.0f);
-    if (hf_gain >= 1.0f) {
-        state.lowpass_2 = 1.0f;
-        state.lowpass_1 = 0.0f;
-    } else {
-        const auto a = 1.0f - hf_gain;
-        const auto b = 2.0f * (2.0f - hf_gain * CosD(256.0f * info.hf_reference /
-                                                     static_cast<f32>(info.sample_rate)));
-        const auto c = std::sqrt(b * b - 4.0f * a * a);
-
-        state.lowpass_1 = (b - c) / (2.0f * a);
-        state.lowpass_2 = 1.0f - state.lowpass_1;
-    }
-    state.early_to_late_taps = AudioCommon::CalculateDelaySamples(
-        sample_rate, 1000.0f * (info.reflection_delay + info.reverb_delay));
-
-    state.last_reverb_echo = 0.6f * info.diffusion * 0.01f;
-    for (std::size_t i = 0; i < AudioCommon::I3DL2REVERB_DELAY_LINE_COUNT; i++) {
-        const auto length =
-            FDN_MIN_DELAY_LINE_TIMES[i] +
-            (info.density / 100.0f) * (FDN_MAX_DELAY_LINE_TIMES[i] - FDN_MIN_DELAY_LINE_TIMES[i]);
-        state.fdn_delay_line[i].SetDelay(AudioCommon::CalculateDelaySamples(sample_rate, length));
-
-        const auto delay_sample_counts = state.fdn_delay_line[i].GetDelay() +
-                                         state.decay_delay_line0[i].GetDelay() +
-                                         state.decay_delay_line1[i].GetDelay();
-
-        float a = (-60.0f * static_cast<f32>(delay_sample_counts)) /
-                  (info.decay_time * static_cast<f32>(info.sample_rate));
-        float b = a / info.hf_decay_ratio;
-        float c = CosD(128.0f * 0.5f * info.hf_reference / static_cast<f32>(info.sample_rate)) /
-                  SinD(128.0f * 0.5f * info.hf_reference / static_cast<f32>(info.sample_rate));
-        float d = Pow10((b - a) / 40.0f);
-        float e = Pow10((b + a) / 40.0f) * 0.7071f;
-
-        state.lpf_coefficients[0][i] = e * ((d * c) + 1.0f) / (c + d);
-        state.lpf_coefficients[1][i] = e * (1.0f - (d * c)) / (c + d);
-        state.lpf_coefficients[2][i] = (c - d) / (c + d);
-
-        state.decay_delay_line0[i].SetCoefficient(state.last_reverb_echo);
-        state.decay_delay_line1[i].SetCoefficient(-0.9f * state.last_reverb_echo);
-    }
-
-    if (should_clear) {
-        for (std::size_t i = 0; i < AudioCommon::I3DL2REVERB_DELAY_LINE_COUNT; i++) {
-            state.fdn_delay_line[i].Clear();
-            state.decay_delay_line0[i].Clear();
-            state.decay_delay_line1[i].Clear();
-        }
-        state.early_delay_line.Clear();
-        state.center_delay_line.Clear();
-    }
-
-    const auto max_early_delay = state.early_delay_line.GetMaxDelay();
-    const auto reflection_time = 1000.0f * (0.9998f * info.reverb_delay + 0.02f);
-    for (std::size_t tap = 0; tap < AudioCommon::I3DL2REVERB_TAPS; tap++) {
-        const auto length = AudioCommon::CalculateDelaySamples(
-            sample_rate, 1000.0f * info.reflection_delay + reflection_time * EARLY_TAP_TIMES[tap]);
-        state.early_tap_steps[tap] = std::min(length, max_early_delay);
-    }
-}
-
-void CommandGenerator::GenerateVolumeRampCommand(float last_volume, float current_volume,
-                                                 s32 channel, s32 node_id) {
-    const auto last = static_cast<s32>(last_volume * 32768.0f);
-    const auto current = static_cast<s32>(current_volume * 32768.0f);
-    const auto delta = static_cast<s32>((static_cast<float>(current) - static_cast<float>(last)) /
-                                        static_cast<float>(worker_params.sample_count));
-
-    if (dumping_frame) {
-        LOG_DEBUG(Audio,
-                  "(DSP_TRACE) GenerateVolumeRampCommand node_id={}, input={}, output={}, "
-                  "last_volume={}, current_volume={}",
-                  node_id, GetMixChannelBufferOffset(channel), GetMixChannelBufferOffset(channel),
-                  last_volume, current_volume);
-    }
-    // Apply generic gain on samples
-    ApplyGain(GetChannelMixBuffer(channel), GetChannelMixBuffer(channel), last, delta,
-              worker_params.sample_count);
-}
-
-void CommandGenerator::GenerateVoiceMixCommand(const MixVolumeBuffer& mix_volumes,
-                                               const MixVolumeBuffer& last_mix_volumes,
-                                               VoiceState& dsp_state, s32 mix_buffer_offset,
-                                               s32 mix_buffer_count, s32 voice_index, s32 node_id) {
-    // Loop all our mix buffers
-    for (s32 i = 0; i < mix_buffer_count; i++) {
-        if (last_mix_volumes[i] != 0.0f || mix_volumes[i] != 0.0f) {
-            const auto delta = static_cast<float>((mix_volumes[i] - last_mix_volumes[i])) /
-                               static_cast<float>(worker_params.sample_count);
-
-            if (dumping_frame) {
-                LOG_DEBUG(Audio,
-                          "(DSP_TRACE) GenerateVoiceMixCommand node_id={}, input={}, "
-                          "output={}, last_volume={}, current_volume={}",
-                          node_id, voice_index, mix_buffer_offset + i, last_mix_volumes[i],
-                          mix_volumes[i]);
-            }
-
-            dsp_state.previous_samples[i] =
-                ApplyMixRamp(GetMixBuffer(mix_buffer_offset + i), GetMixBuffer(voice_index),
-                             last_mix_volumes[i], delta, worker_params.sample_count);
-        } else {
-            dsp_state.previous_samples[i] = 0;
-        }
-    }
-}
-
-void CommandGenerator::GenerateSubMixCommand(ServerMixInfo& mix_info) {
-    if (dumping_frame) {
-        LOG_DEBUG(Audio, "(DSP_TRACE) GenerateSubMixCommand");
-    }
-    const auto& in_params = mix_info.GetInParams();
-    GenerateDepopForMixBuffersCommand(in_params.buffer_count, in_params.buffer_offset,
-                                      in_params.sample_rate);
-
-    GenerateEffectCommand(mix_info);
-
-    GenerateMixCommands(mix_info);
-}
-
-void CommandGenerator::GenerateMixCommands(ServerMixInfo& mix_info) {
-    if (!mix_info.HasAnyConnection()) {
-        return;
-    }
-    const auto& in_params = mix_info.GetInParams();
-    if (in_params.dest_mix_id != AudioCommon::NO_MIX) {
-        const auto& dest_mix = mix_context.GetInfo(in_params.dest_mix_id);
-        const auto& dest_in_params = dest_mix.GetInParams();
-
-        const auto buffer_count = in_params.buffer_count;
-
-        for (s32 i = 0; i < buffer_count; i++) {
-            for (s32 j = 0; j < dest_in_params.buffer_count; j++) {
-                const auto mixed_volume = in_params.volume * in_params.mix_volume[i][j];
-                if (mixed_volume != 0.0f) {
-                    GenerateMixCommand(dest_in_params.buffer_offset + j,
-                                       in_params.buffer_offset + i, mixed_volume,
-                                       in_params.node_id);
-                }
-            }
-        }
-    } else if (in_params.splitter_id != AudioCommon::NO_SPLITTER) {
-        s32 base{};
-        while (const auto* destination_data = GetDestinationData(in_params.splitter_id, base++)) {
-            if (!destination_data->IsConfigured()) {
-                continue;
-            }
-
-            const auto& dest_mix = mix_context.GetInfo(destination_data->GetMixId());
-            const auto& dest_in_params = dest_mix.GetInParams();
-            const auto mix_index = (base - 1) % in_params.buffer_count + in_params.buffer_offset;
-            for (std::size_t i = 0; i < static_cast<std::size_t>(dest_in_params.buffer_count);
-                 i++) {
-                const auto mixed_volume = in_params.volume * destination_data->GetMixVolume(i);
-                if (mixed_volume != 0.0f) {
-                    GenerateMixCommand(dest_in_params.buffer_offset + i, mix_index, mixed_volume,
-                                       in_params.node_id);
-                }
-            }
-        }
-    }
-}
-
-void CommandGenerator::GenerateMixCommand(std::size_t output_offset, std::size_t input_offset,
-                                          float volume, s32 node_id) {
-
-    if (dumping_frame) {
-        LOG_DEBUG(Audio,
-                  "(DSP_TRACE) GenerateMixCommand node_id={}, input={}, output={}, volume={}",
-                  node_id, input_offset, output_offset, volume);
-    }
-
-    std::span<s32> output = GetMixBuffer(output_offset);
-    std::span<const s32> input = GetMixBuffer(input_offset);
-
-    const s32 gain = static_cast<s32>(volume * 32768.0f);
-    // Mix with loop unrolling
-    if (worker_params.sample_count % 4 == 0) {
-        ApplyMix<4>(output, input, gain, worker_params.sample_count);
-    } else if (worker_params.sample_count % 2 == 0) {
-        ApplyMix<2>(output, input, gain, worker_params.sample_count);
-    } else {
-        ApplyMix<1>(output, input, gain, worker_params.sample_count);
-    }
-}
-
-void CommandGenerator::GenerateFinalMixCommand() {
-    if (dumping_frame) {
-        LOG_DEBUG(Audio, "(DSP_TRACE) GenerateFinalMixCommand");
-    }
-    auto& mix_info = mix_context.GetFinalMixInfo();
-    const auto& in_params = mix_info.GetInParams();
-
-    GenerateDepopForMixBuffersCommand(in_params.buffer_count, in_params.buffer_offset,
-                                      in_params.sample_rate);
-
-    GenerateEffectCommand(mix_info);
-
-    for (s32 i = 0; i < in_params.buffer_count; i++) {
-        const s32 gain = static_cast<s32>(in_params.volume * 32768.0f);
-        if (dumping_frame) {
-            LOG_DEBUG(
-                Audio,
-                "(DSP_TRACE) ApplyGainWithoutDelta node_id={}, input={}, output={}, volume={}",
-                in_params.node_id, in_params.buffer_offset + i, in_params.buffer_offset + i,
-                in_params.volume);
-        }
-        ApplyGainWithoutDelta(GetMixBuffer(in_params.buffer_offset + i),
-                              GetMixBuffer(in_params.buffer_offset + i), gain,
-                              worker_params.sample_count);
-    }
-}
-
-template <typename T>
-s32 CommandGenerator::DecodePcm(ServerVoiceInfo& voice_info, VoiceState& dsp_state,
-                                s32 sample_start_offset, s32 sample_end_offset, s32 sample_count,
-                                s32 channel, std::size_t mix_offset) {
-    const auto& in_params = voice_info.GetInParams();
-    const auto& wave_buffer = in_params.wave_buffer[dsp_state.wave_buffer_index];
-    if (wave_buffer.buffer_address == 0) {
-        return 0;
-    }
-    if (wave_buffer.buffer_size == 0) {
-        return 0;
-    }
-    if (sample_end_offset < sample_start_offset) {
-        return 0;
-    }
-    const auto samples_remaining = (sample_end_offset - sample_start_offset) - dsp_state.offset;
-    const auto start_offset =
-        ((dsp_state.offset + sample_start_offset) * in_params.channel_count) * sizeof(T);
-    const auto buffer_pos = wave_buffer.buffer_address + start_offset;
-    const auto samples_processed = std::min(sample_count, samples_remaining);
-
-    const auto channel_count = in_params.channel_count;
-    std::vector<T> buffer(samples_processed * channel_count);
-    memory.ReadBlock(buffer_pos, buffer.data(), buffer.size() * sizeof(T));
-
-    if constexpr (std::is_floating_point_v<T>) {
-        for (std::size_t i = 0; i < static_cast<std::size_t>(samples_processed); i++) {
-            sample_buffer[mix_offset + i] = static_cast<s32>(buffer[i * channel_count + channel] *
-                                                             std::numeric_limits<s16>::max());
-        }
-    } else if constexpr (sizeof(T) == 1) {
-        for (std::size_t i = 0; i < static_cast<std::size_t>(samples_processed); i++) {
-            sample_buffer[mix_offset + i] =
-                static_cast<s32>(static_cast<f32>(buffer[i * channel_count + channel] /
-                                                  std::numeric_limits<s8>::max()) *
-                                 std::numeric_limits<s16>::max());
-        }
-    } else if constexpr (sizeof(T) == 2) {
-        for (std::size_t i = 0; i < static_cast<std::size_t>(samples_processed); i++) {
-            sample_buffer[mix_offset + i] = buffer[i * channel_count + channel];
-        }
-    } else {
-        for (std::size_t i = 0; i < static_cast<std::size_t>(samples_processed); i++) {
-            sample_buffer[mix_offset + i] =
-                static_cast<s32>(static_cast<f32>(buffer[i * channel_count + channel] /
-                                                  std::numeric_limits<s32>::max()) *
-                                 std::numeric_limits<s16>::max());
-        }
-    }
-
-    return samples_processed;
-}
-
-s32 CommandGenerator::DecodeAdpcm(ServerVoiceInfo& voice_info, VoiceState& dsp_state,
-                                  s32 sample_start_offset, s32 sample_end_offset, s32 sample_count,
-                                  [[maybe_unused]] s32 channel, std::size_t mix_offset) {
-    const auto& in_params = voice_info.GetInParams();
-    const auto& wave_buffer = in_params.wave_buffer[dsp_state.wave_buffer_index];
-    if (wave_buffer.buffer_address == 0) {
-        return 0;
-    }
-    if (wave_buffer.buffer_size == 0) {
-        return 0;
-    }
-    if (sample_end_offset < sample_start_offset) {
-        return 0;
-    }
-
-    static constexpr std::array<int, 16> SIGNED_NIBBLES{
-        0, 1, 2, 3, 4, 5, 6, 7, -8, -7, -6, -5, -4, -3, -2, -1,
-    };
-
-    constexpr std::size_t FRAME_LEN = 8;
-    constexpr std::size_t NIBBLES_PER_SAMPLE = 16;
-    constexpr std::size_t SAMPLES_PER_FRAME = 14;
-
-    auto frame_header = dsp_state.context.header;
-    s32 idx = (frame_header >> 4) & 0xf;
-    s32 scale = frame_header & 0xf;
-    s16 yn1 = dsp_state.context.yn1;
-    s16 yn2 = dsp_state.context.yn2;
-
-    Codec::ADPCM_Coeff coeffs;
-    memory.ReadBlock(in_params.additional_params_address, coeffs.data(),
-                     sizeof(Codec::ADPCM_Coeff));
-
-    s32 coef1 = coeffs[idx * 2];
-    s32 coef2 = coeffs[idx * 2 + 1];
-
-    const auto samples_remaining = (sample_end_offset - sample_start_offset) - dsp_state.offset;
-    const auto samples_processed = std::min(sample_count, samples_remaining);
-    const auto sample_pos = dsp_state.offset + sample_start_offset;
-
-    const auto samples_remaining_in_frame = sample_pos % SAMPLES_PER_FRAME;
-    auto position_in_frame = ((sample_pos / SAMPLES_PER_FRAME) * NIBBLES_PER_SAMPLE) +
-                             samples_remaining_in_frame + (samples_remaining_in_frame != 0 ? 2 : 0);
-
-    const auto decode_sample = [&](const int nibble) -> s16 {
-        const int xn = nibble * (1 << scale);
-        // We first transform everything into 11 bit fixed point, perform the second order
-        // digital filter, then transform back.
-        // 0x400 == 0.5 in 11 bit fixed point.
-        // Filter: y[n] = x[n] + 0.5 + c1 * y[n-1] + c2 * y[n-2]
-        int val = ((xn << 11) + 0x400 + coef1 * yn1 + coef2 * yn2) >> 11;
-        // Clamp to output range.
-        val = std::clamp<s32>(val, -32768, 32767);
-        // Advance output feedback.
-        yn2 = yn1;
-        yn1 = static_cast<s16>(val);
-        return yn1;
-    };
-
-    std::size_t buffer_offset{};
-    std::vector<u8> buffer(
-        std::max((samples_processed / FRAME_LEN) * SAMPLES_PER_FRAME, FRAME_LEN));
-    memory.ReadBlock(wave_buffer.buffer_address + (position_in_frame / 2), buffer.data(),
-                     buffer.size());
-    std::size_t cur_mix_offset = mix_offset;
-
-    auto remaining_samples = samples_processed;
-    while (remaining_samples > 0) {
-        if (position_in_frame % NIBBLES_PER_SAMPLE == 0) {
-            // Read header
-            frame_header = buffer[buffer_offset++];
-            idx = (frame_header >> 4) & 0xf;
-            scale = frame_header & 0xf;
-            coef1 = coeffs[idx * 2];
-            coef2 = coeffs[idx * 2 + 1];
-            position_in_frame += 2;
-
-            // Decode entire frame
-            if (remaining_samples >= static_cast<int>(SAMPLES_PER_FRAME)) {
-                for (std::size_t i = 0; i < SAMPLES_PER_FRAME / 2; i++) {
-                    // Sample 1
-                    const s32 s0 = SIGNED_NIBBLES[buffer[buffer_offset] >> 4];
-                    const s32 s1 = SIGNED_NIBBLES[buffer[buffer_offset++] & 0xf];
-                    const s16 sample_1 = decode_sample(s0);
-                    const s16 sample_2 = decode_sample(s1);
-                    sample_buffer[cur_mix_offset++] = sample_1;
-                    sample_buffer[cur_mix_offset++] = sample_2;
-                }
-                remaining_samples -= static_cast<int>(SAMPLES_PER_FRAME);
-                position_in_frame += SAMPLES_PER_FRAME;
-                continue;
-            }
-        }
-        // Decode mid frame
-        s32 current_nibble = buffer[buffer_offset];
-        if (position_in_frame++ & 0x1) {
-            current_nibble &= 0xf;
-            buffer_offset++;
-        } else {
-            current_nibble >>= 4;
-        }
-        const s16 sample = decode_sample(SIGNED_NIBBLES[current_nibble]);
-        sample_buffer[cur_mix_offset++] = sample;
-        remaining_samples--;
-    }
-
-    dsp_state.context.header = frame_header;
-    dsp_state.context.yn1 = yn1;
-    dsp_state.context.yn2 = yn2;
-
-    return samples_processed;
-}
-
-std::span<s32> CommandGenerator::GetMixBuffer(std::size_t index) {
-    return std::span<s32>(mix_buffer.data() + (index * worker_params.sample_count),
-                          worker_params.sample_count);
-}
-
-std::span<const s32> CommandGenerator::GetMixBuffer(std::size_t index) const {
-    return std::span<const s32>(mix_buffer.data() + (index * worker_params.sample_count),
-                                worker_params.sample_count);
-}
-
-std::size_t CommandGenerator::GetMixChannelBufferOffset(s32 channel) const {
-    return worker_params.mix_buffer_count + channel;
-}
-
-std::size_t CommandGenerator::GetTotalMixBufferCount() const {
-    return worker_params.mix_buffer_count + AudioCommon::MAX_CHANNEL_COUNT;
-}
-
-std::span<s32> CommandGenerator::GetChannelMixBuffer(s32 channel) {
-    return GetMixBuffer(worker_params.mix_buffer_count + channel);
-}
-
-std::span<const s32> CommandGenerator::GetChannelMixBuffer(s32 channel) const {
-    return GetMixBuffer(worker_params.mix_buffer_count + channel);
-}
-
-void CommandGenerator::DecodeFromWaveBuffers(ServerVoiceInfo& voice_info, std::span<s32> output,
-                                             VoiceState& dsp_state, s32 channel,
-                                             s32 target_sample_rate, s32 sample_count,
-                                             s32 node_id) {
-    const auto& in_params = voice_info.GetInParams();
-    if (dumping_frame) {
-        LOG_DEBUG(Audio,
-                  "(DSP_TRACE) DecodeFromWaveBuffers, node_id={}, channel={}, "
-                  "format={}, sample_count={}, sample_rate={}, mix_id={}, splitter_id={}",
-                  node_id, channel, in_params.sample_format, sample_count, in_params.sample_rate,
-                  in_params.mix_id, in_params.splitter_info_id);
-    }
-    ASSERT_OR_EXECUTE(output.data() != nullptr, { return; });
-
-    const auto resample_rate = static_cast<s32>(
-        static_cast<float>(in_params.sample_rate) / static_cast<float>(target_sample_rate) *
-        static_cast<float>(static_cast<s32>(in_params.pitch * 32768.0f)));
-    if (dsp_state.fraction + sample_count * resample_rate >
-        static_cast<s32>(SCALED_MIX_BUFFER_SIZE - 4ULL)) {
-        return;
-    }
-
-    auto min_required_samples =
-        std::min(static_cast<s32>(SCALED_MIX_BUFFER_SIZE) - dsp_state.fraction, resample_rate);
-    if (min_required_samples >= sample_count) {
-        min_required_samples = sample_count;
-    }
-
-    std::size_t temp_mix_offset{};
-    s32 samples_output{};
-    auto samples_remaining = sample_count;
-    while (samples_remaining > 0) {
-        const auto samples_to_output = std::min(samples_remaining, min_required_samples);
-        const auto samples_to_read = (samples_to_output * resample_rate + dsp_state.fraction) >> 15;
-
-        if (!in_params.behavior_flags.is_pitch_and_src_skipped) {
-            // Append sample histtory for resampler
-            for (std::size_t i = 0; i < AudioCommon::MAX_SAMPLE_HISTORY; i++) {
-                sample_buffer[temp_mix_offset + i] = dsp_state.sample_history[i];
-            }
-            temp_mix_offset += 4;
-        }
-
-        s32 samples_read{};
-        while (samples_read < samples_to_read) {
-            const auto& wave_buffer = in_params.wave_buffer[dsp_state.wave_buffer_index];
-            // No more data can be read
-            if (!dsp_state.is_wave_buffer_valid[dsp_state.wave_buffer_index]) {
-                break;
-            }
-
-            if (in_params.sample_format == SampleFormat::Adpcm && dsp_state.offset == 0 &&
-                wave_buffer.context_address != 0 && wave_buffer.context_size != 0) {
-                memory.ReadBlock(wave_buffer.context_address, &dsp_state.context,
-                                 sizeof(ADPCMContext));
-            }
-
-            s32 samples_offset_start;
-            s32 samples_offset_end;
-            if (dsp_state.loop_count > 0 && wave_buffer.loop_start_sample != 0 &&
-                wave_buffer.loop_end_sample != 0 &&
-                wave_buffer.loop_start_sample <= wave_buffer.loop_end_sample) {
-                samples_offset_start = wave_buffer.loop_start_sample;
-                samples_offset_end = wave_buffer.loop_end_sample;
-            } else {
-                samples_offset_start = wave_buffer.start_sample_offset;
-                samples_offset_end = wave_buffer.end_sample_offset;
-            }
-
-            s32 samples_decoded{0};
-            switch (in_params.sample_format) {
-            case SampleFormat::Pcm8:
-                samples_decoded =
-                    DecodePcm<s8>(voice_info, dsp_state, samples_offset_start, samples_offset_end,
-                                  samples_to_read - samples_read, channel, temp_mix_offset);
-                break;
-            case SampleFormat::Pcm16:
-                samples_decoded =
-                    DecodePcm<s16>(voice_info, dsp_state, samples_offset_start, samples_offset_end,
-                                   samples_to_read - samples_read, channel, temp_mix_offset);
-                break;
-            case SampleFormat::Pcm32:
-                samples_decoded =
-                    DecodePcm<s32>(voice_info, dsp_state, samples_offset_start, samples_offset_end,
-                                   samples_to_read - samples_read, channel, temp_mix_offset);
-                break;
-            case SampleFormat::PcmFloat:
-                samples_decoded =
-                    DecodePcm<f32>(voice_info, dsp_state, samples_offset_start, samples_offset_end,
-                                   samples_to_read - samples_read, channel, temp_mix_offset);
-                break;
-            case SampleFormat::Adpcm:
-                samples_decoded =
-                    DecodeAdpcm(voice_info, dsp_state, samples_offset_start, samples_offset_end,
-                                samples_to_read - samples_read, channel, temp_mix_offset);
-                break;
-            default:
-                ASSERT_MSG(false, "Unimplemented sample format={}", in_params.sample_format);
-            }
-
-            temp_mix_offset += samples_decoded;
-            samples_read += samples_decoded;
-            dsp_state.offset += samples_decoded;
-            dsp_state.played_sample_count += samples_decoded;
-
-            if (dsp_state.offset >= (samples_offset_end - samples_offset_start) ||
-                samples_decoded == 0) {
-                // Reset our sample offset
-                dsp_state.offset = 0;
-                if (wave_buffer.is_looping) {
-                    dsp_state.loop_count++;
-                    if (wave_buffer.loop_count > 0 &&
-                        (dsp_state.loop_count > wave_buffer.loop_count || samples_decoded == 0)) {
-                        // End of our buffer
-                        voice_info.SetWaveBufferCompleted(dsp_state, wave_buffer);
-                    }
-
-                    if (samples_decoded == 0) {
-                        break;
-                    }
-
-                    if (in_params.behavior_flags.is_played_samples_reset_at_loop_point.Value()) {
-                        dsp_state.played_sample_count = 0;
-                    }
-                } else {
-                    // Update our wave buffer states
-                    voice_info.SetWaveBufferCompleted(dsp_state, wave_buffer);
-                }
-            }
-        }
-
-        if (in_params.behavior_flags.is_pitch_and_src_skipped.Value()) {
-            // No need to resample
-            std::memcpy(output.data() + samples_output, sample_buffer.data(),
-                        samples_read * sizeof(s32));
-        } else {
-            std::fill(sample_buffer.begin() + temp_mix_offset,
-                      sample_buffer.begin() + temp_mix_offset + (samples_to_read - samples_read),
-                      0);
-            AudioCore::Resample(output.data() + samples_output, sample_buffer.data(), resample_rate,
-                                dsp_state.fraction, samples_to_output);
-            // Resample
-            for (std::size_t i = 0; i < AudioCommon::MAX_SAMPLE_HISTORY; i++) {
-                dsp_state.sample_history[i] = sample_buffer[samples_to_read + i];
-            }
-        }
-        samples_remaining -= samples_to_output;
-        samples_output += samples_to_output;
-    }
-}
-
-} // namespace AudioCore
diff --git a/src/audio_core/command_generator.h b/src/audio_core/command_generator.h
deleted file mode 100644
index 8077e77682..0000000000
--- a/src/audio_core/command_generator.h
+++ /dev/null
@@ -1,110 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include <array>
-#include <span>
-#include "audio_core/common.h"
-#include "audio_core/voice_context.h"
-#include "common/common_types.h"
-
-namespace Core::Memory {
-class Memory;
-}
-
-namespace AudioCore {
-class MixContext;
-class SplitterContext;
-class ServerSplitterDestinationData;
-class ServerMixInfo;
-class EffectContext;
-class EffectBase;
-struct AuxInfoDSP;
-struct I3dl2ReverbParams;
-struct I3dl2ReverbState;
-using MixVolumeBuffer = std::array<float, AudioCommon::MAX_MIX_BUFFERS>;
-
-class CommandGenerator {
-public:
-    explicit CommandGenerator(AudioCommon::AudioRendererParameter& worker_params_,
-                              VoiceContext& voice_context_, MixContext& mix_context_,
-                              SplitterContext& splitter_context_, EffectContext& effect_context_,
-                              Core::Memory::Memory& memory_);
-    ~CommandGenerator();
-
-    void ClearMixBuffers();
-    void GenerateVoiceCommands();
-    void GenerateVoiceCommand(ServerVoiceInfo& voice_info);
-    void GenerateSubMixCommands();
-    void GenerateFinalMixCommands();
-    void PreCommand();
-    void PostCommand();
-
-    [[nodiscard]] std::span<s32> GetChannelMixBuffer(s32 channel);
-    [[nodiscard]] std::span<const s32> GetChannelMixBuffer(s32 channel) const;
-    [[nodiscard]] std::span<s32> GetMixBuffer(std::size_t index);
-    [[nodiscard]] std::span<const s32> GetMixBuffer(std::size_t index) const;
-    [[nodiscard]] std::size_t GetMixChannelBufferOffset(s32 channel) const;
-
-    [[nodiscard]] std::size_t GetTotalMixBufferCount() const;
-
-private:
-    void GenerateDataSourceCommand(ServerVoiceInfo& voice_info, VoiceState& dsp_state, s32 channel);
-    void GenerateBiquadFilterCommandForVoice(ServerVoiceInfo& voice_info, VoiceState& dsp_state,
-                                             s32 mix_buffer_count, s32 channel);
-    void GenerateVolumeRampCommand(float last_volume, float current_volume, s32 channel,
-                                   s32 node_id);
-    void GenerateVoiceMixCommand(const MixVolumeBuffer& mix_volumes,
-                                 const MixVolumeBuffer& last_mix_volumes, VoiceState& dsp_state,
-                                 s32 mix_buffer_offset, s32 mix_buffer_count, s32 voice_index,
-                                 s32 node_id);
-    void GenerateSubMixCommand(ServerMixInfo& mix_info);
-    void GenerateMixCommands(ServerMixInfo& mix_info);
-    void GenerateMixCommand(std::size_t output_offset, std::size_t input_offset, float volume,
-                            s32 node_id);
-    void GenerateFinalMixCommand();
-    void GenerateBiquadFilterCommand(s32 mix_buffer, const BiquadFilterParameter& params,
-                                     std::array<s64, 2>& state, std::size_t input_offset,
-                                     std::size_t output_offset, s32 sample_count, s32 node_id);
-    void GenerateDepopPrepareCommand(VoiceState& dsp_state, std::size_t mix_buffer_count,
-                                     std::size_t mix_buffer_offset);
-    void GenerateDepopForMixBuffersCommand(std::size_t mix_buffer_count,
-                                           std::size_t mix_buffer_offset, s32 sample_rate);
-    void GenerateEffectCommand(ServerMixInfo& mix_info);
-    void GenerateI3dl2ReverbEffectCommand(s32 mix_buffer_offset, EffectBase* info, bool enabled);
-    void GenerateBiquadFilterEffectCommand(s32 mix_buffer_offset, EffectBase* info, bool enabled);
-    void GenerateAuxCommand(s32 mix_buffer_offset, EffectBase* info, bool enabled);
-    [[nodiscard]] ServerSplitterDestinationData* GetDestinationData(s32 splitter_id, s32 index);
-
-    s32 WriteAuxBuffer(AuxInfoDSP& dsp_info, VAddr send_buffer, u32 max_samples,
-                       std::span<const s32> data, u32 sample_count, u32 write_offset,
-                       u32 write_count);
-    s32 ReadAuxBuffer(AuxInfoDSP& recv_info, VAddr recv_buffer, u32 max_samples,
-                      std::span<s32> out_data, u32 sample_count, u32 read_offset, u32 read_count);
-
-    void InitializeI3dl2Reverb(I3dl2ReverbParams& info, I3dl2ReverbState& state,
-                               std::vector<u8>& work_buffer);
-    void UpdateI3dl2Reverb(I3dl2ReverbParams& info, I3dl2ReverbState& state, bool should_clear);
-    // DSP Code
-    template <typename T>
-    s32 DecodePcm(ServerVoiceInfo& voice_info, VoiceState& dsp_state, s32 sample_start_offset,
-                  s32 sample_end_offset, s32 sample_count, s32 channel, std::size_t mix_offset);
-    s32 DecodeAdpcm(ServerVoiceInfo& voice_info, VoiceState& dsp_state, s32 sample_start_offset,
-                    s32 sample_end_offset, s32 sample_count, s32 channel, std::size_t mix_offset);
-    void DecodeFromWaveBuffers(ServerVoiceInfo& voice_info, std::span<s32> output,
-                               VoiceState& dsp_state, s32 channel, s32 target_sample_rate,
-                               s32 sample_count, s32 node_id);
-
-    AudioCommon::AudioRendererParameter& worker_params;
-    VoiceContext& voice_context;
-    MixContext& mix_context;
-    SplitterContext& splitter_context;
-    EffectContext& effect_context;
-    Core::Memory::Memory& memory;
-    std::vector<s32> mix_buffer{};
-    std::vector<s32> sample_buffer{};
-    std::vector<s32> depop_buffer{};
-    bool dumping_frame{false};
-};
-} // namespace AudioCore
diff --git a/src/audio_core/common.h b/src/audio_core/common.h
deleted file mode 100644
index 056a0ac707..0000000000
--- a/src/audio_core/common.h
+++ /dev/null
@@ -1,132 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include "common/common_funcs.h"
-#include "common/common_types.h"
-#include "common/swap.h"
-#include "core/hle/result.h"
-
-namespace AudioCommon {
-namespace Audren {
-constexpr Result ERR_INVALID_PARAMETERS{ErrorModule::Audio, 41};
-constexpr Result ERR_SPLITTER_SORT_FAILED{ErrorModule::Audio, 43};
-} // namespace Audren
-
-constexpr u8 BASE_REVISION = '0';
-constexpr u32_le CURRENT_PROCESS_REVISION =
-    Common::MakeMagic('R', 'E', 'V', static_cast<u8>(BASE_REVISION + 0xA));
-constexpr std::size_t MAX_MIX_BUFFERS = 24;
-constexpr std::size_t MAX_BIQUAD_FILTERS = 2;
-constexpr std::size_t MAX_CHANNEL_COUNT = 6;
-constexpr std::size_t MAX_WAVE_BUFFERS = 4;
-constexpr std::size_t MAX_SAMPLE_HISTORY = 4;
-constexpr u32 STREAM_SAMPLE_RATE = 48000;
-constexpr u32 STREAM_NUM_CHANNELS = 2;
-constexpr s32 NO_SPLITTER = -1;
-constexpr s32 NO_MIX = 0x7fffffff;
-constexpr s32 NO_FINAL_MIX = std::numeric_limits<s32>::min();
-constexpr s32 FINAL_MIX = 0;
-constexpr s32 NO_EFFECT_ORDER = -1;
-constexpr std::size_t TEMP_MIX_BASE_SIZE = 0x3f00; // TODO(ogniK): Work out this constant
-// Any size checks seem to take the sample history into account
-// and our const ends up being 0x3f04, the 4 bytes are most
-// likely the sample history
-constexpr std::size_t TOTAL_TEMP_MIX_SIZE = TEMP_MIX_BASE_SIZE + AudioCommon::MAX_SAMPLE_HISTORY;
-constexpr f32 I3DL2REVERB_MAX_LEVEL = 5000.0f;
-constexpr f32 I3DL2REVERB_MIN_REFLECTION_DURATION = 0.02f;
-constexpr std::size_t I3DL2REVERB_TAPS = 20;
-constexpr std::size_t I3DL2REVERB_DELAY_LINE_COUNT = 4;
-using Fractional = s32;
-
-template <typename T>
-constexpr Fractional ToFractional(T x) {
-    return static_cast<Fractional>(x * static_cast<T>(0x4000));
-}
-
-constexpr Fractional MultiplyFractional(Fractional lhs, Fractional rhs) {
-    return static_cast<Fractional>(static_cast<s64>(lhs) * rhs >> 14);
-}
-
-constexpr s32 FractionalToFixed(Fractional x) {
-    const auto s = x & (1 << 13);
-    return static_cast<s32>(x >> 14) + s;
-}
-
-constexpr s32 CalculateDelaySamples(s32 sample_rate_khz, float time) {
-    return FractionalToFixed(MultiplyFractional(ToFractional(sample_rate_khz), ToFractional(time)));
-}
-
-static constexpr u32 VersionFromRevision(u32_le rev) {
-    // "REV7" -> 7
-    return ((rev >> 24) & 0xff) - 0x30;
-}
-
-static constexpr bool IsRevisionSupported(u32 required, u32_le user_revision) {
-    const auto base = VersionFromRevision(user_revision);
-    return required <= base;
-}
-
-static constexpr bool IsValidRevision(u32_le revision) {
-    const auto base = VersionFromRevision(revision);
-    constexpr auto max_rev = VersionFromRevision(CURRENT_PROCESS_REVISION);
-    return base <= max_rev;
-}
-
-static constexpr bool CanConsumeBuffer(std::size_t size, std::size_t offset, std::size_t required) {
-    if (offset > size) {
-        return false;
-    }
-    if (size < required) {
-        return false;
-    }
-    if ((size - offset) < required) {
-        return false;
-    }
-    return true;
-}
-
-struct UpdateDataSizes {
-    u32_le behavior{};
-    u32_le memory_pool{};
-    u32_le voice{};
-    u32_le voice_channel_resource{};
-    u32_le effect{};
-    u32_le mixer{};
-    u32_le sink{};
-    u32_le performance{};
-    u32_le splitter{};
-    u32_le render_info{};
-    INSERT_PADDING_WORDS(4);
-};
-static_assert(sizeof(UpdateDataSizes) == 0x38, "UpdateDataSizes is an invalid size");
-
-struct UpdateDataHeader {
-    u32_le revision{};
-    UpdateDataSizes size{};
-    u32_le total_size{};
-};
-static_assert(sizeof(UpdateDataHeader) == 0x40, "UpdateDataHeader is an invalid size");
-
-struct AudioRendererParameter {
-    u32_le sample_rate;
-    u32_le sample_count;
-    u32_le mix_buffer_count;
-    u32_le submix_count;
-    u32_le voice_count;
-    u32_le sink_count;
-    u32_le effect_count;
-    u32_le performance_frame_count;
-    u8 is_voice_drop_enabled;
-    u8 unknown_21;
-    u8 unknown_22;
-    u8 execution_mode;
-    u32_le splitter_count;
-    u32_le num_splitter_send_channels;
-    u32_le unknown_30;
-    u32_le revision;
-};
-static_assert(sizeof(AudioRendererParameter) == 52, "AudioRendererParameter is an invalid size");
-
-} // namespace AudioCommon
diff --git a/src/audio_core/common/audio_renderer_parameter.h b/src/audio_core/common/audio_renderer_parameter.h
new file mode 100644
index 0000000000..2f62c383b8
--- /dev/null
+++ b/src/audio_core/common/audio_renderer_parameter.h
@@ -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 */ u16 voice_drop_enabled;
+    /* 0x22 */ u8 rendering_device;
+    /* 0x23 */ ExecutionMode execution_mode;
+    /* 0x24 */ u32 splitter_infos;
+    /* 0x28 */ s32 splitter_destinations;
+    /* 0x2C */ u32 external_context_size;
+    /* 0x30 */ u32 revision;
+    /* 0x34 */ char unk34[0x4];
+};
+static_assert(sizeof(AudioRendererParameterInternal) == 0x38,
+              "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;
+    AudioRenderer::BehaviorInfo* behavior;
+    std::span<s32> depop_buffer;
+    AudioRenderer::UpsamplerManager* upsampler_manager;
+    AudioRenderer::MemoryPoolInfo* memory_pool_info;
+};
+
+} // namespace AudioCore
diff --git a/src/audio_core/common/common.h b/src/audio_core/common/common.h
new file mode 100644
index 0000000000..6abd9be45e
--- /dev/null
+++ b/src/audio_core/common/common.h
@@ -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
diff --git a/src/audio_core/common/feature_support.h b/src/audio_core/common/feature_support.h
new file mode 100644
index 0000000000..55c9e690df
--- /dev/null
+++ b/src/audio_core/common/feature_support.h
@@ -0,0 +1,105 @@
+// 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"
+
+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
diff --git a/src/audio_core/common/wave_buffer.h b/src/audio_core/common/wave_buffer.h
new file mode 100644
index 0000000000..fc478ef798
--- /dev/null
+++ b/src/audio_core/common/wave_buffer.h
@@ -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
diff --git a/src/audio_core/common/workbuffer_allocator.h b/src/audio_core/common/workbuffer_allocator.h
new file mode 100644
index 0000000000..fb89f97fea
--- /dev/null
+++ b/src/audio_core/common/workbuffer_allocator.h
@@ -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
diff --git a/src/audio_core/cubeb_sink.cpp b/src/audio_core/cubeb_sink.cpp
deleted file mode 100644
index e48c1ee8eb..0000000000
--- a/src/audio_core/cubeb_sink.cpp
+++ /dev/null
@@ -1,249 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include <algorithm>
-#include <atomic>
-#include <cstring>
-#include "audio_core/cubeb_sink.h"
-#include "audio_core/stream.h"
-#include "common/assert.h"
-#include "common/logging/log.h"
-#include "common/ring_buffer.h"
-#include "common/settings.h"
-
-#ifdef _WIN32
-#include <objbase.h>
-#endif
-
-namespace AudioCore {
-
-class CubebSinkStream final : public SinkStream {
-public:
-    CubebSinkStream(cubeb* ctx_, u32 sample_rate, u32 num_channels_, cubeb_devid output_device,
-                    const std::string& name)
-        : ctx{ctx_}, num_channels{std::min(num_channels_, 6u)} {
-
-        cubeb_stream_params params{};
-        params.rate = sample_rate;
-        params.channels = num_channels;
-        params.format = CUBEB_SAMPLE_S16NE;
-        params.prefs = CUBEB_STREAM_PREF_PERSIST;
-        switch (num_channels) {
-        case 1:
-            params.layout = CUBEB_LAYOUT_MONO;
-            break;
-        case 2:
-            params.layout = CUBEB_LAYOUT_STEREO;
-            break;
-        case 6:
-            params.layout = CUBEB_LAYOUT_3F2_LFE;
-            break;
-        }
-
-        u32 minimum_latency{};
-        if (cubeb_get_min_latency(ctx, &params, &minimum_latency) != CUBEB_OK) {
-            LOG_CRITICAL(Audio_Sink, "Error getting minimum latency");
-        }
-
-        if (cubeb_stream_init(ctx, &stream_backend, name.c_str(), nullptr, nullptr, output_device,
-                              &params, std::max(512u, minimum_latency),
-                              &CubebSinkStream::DataCallback, &CubebSinkStream::StateCallback,
-                              this) != CUBEB_OK) {
-            LOG_CRITICAL(Audio_Sink, "Error initializing cubeb stream");
-            return;
-        }
-
-        if (cubeb_stream_start(stream_backend) != CUBEB_OK) {
-            LOG_CRITICAL(Audio_Sink, "Error starting cubeb stream");
-            return;
-        }
-    }
-
-    ~CubebSinkStream() override {
-        if (!ctx) {
-            return;
-        }
-
-        if (cubeb_stream_stop(stream_backend) != CUBEB_OK) {
-            LOG_CRITICAL(Audio_Sink, "Error stopping cubeb stream");
-        }
-
-        cubeb_stream_destroy(stream_backend);
-    }
-
-    void EnqueueSamples(u32 source_num_channels, const std::vector<s16>& samples) override {
-        if (source_num_channels > num_channels) {
-            // Downsample 6 channels to 2
-            ASSERT_MSG(source_num_channels == 6, "Channel count must be 6");
-
-            std::vector<s16> buf;
-            buf.reserve(samples.size() * num_channels / source_num_channels);
-            for (std::size_t i = 0; i < samples.size(); i += source_num_channels) {
-                // Downmixing implementation taken from the ATSC standard
-                const s16 left{samples[i + 0]};
-                const s16 right{samples[i + 1]};
-                const s16 center{samples[i + 2]};
-                const s16 surround_left{samples[i + 4]};
-                const s16 surround_right{samples[i + 5]};
-                // Not used in the ATSC reference implementation
-                [[maybe_unused]] const s16 low_frequency_effects{samples[i + 3]};
-
-                constexpr s32 clev{707}; // center mixing level coefficient
-                constexpr s32 slev{707}; // surround mixing level coefficient
-
-                buf.push_back(static_cast<s16>(left + (clev * center / 1000) +
-                                               (slev * surround_left / 1000)));
-                buf.push_back(static_cast<s16>(right + (clev * center / 1000) +
-                                               (slev * surround_right / 1000)));
-            }
-            queue.Push(buf);
-            return;
-        }
-
-        queue.Push(samples);
-    }
-
-    std::size_t SamplesInQueue(u32 channel_count) const override {
-        if (!ctx)
-            return 0;
-
-        return queue.Size() / channel_count;
-    }
-
-    void Flush() override {
-        should_flush = true;
-    }
-
-    u32 GetNumChannels() const {
-        return num_channels;
-    }
-
-private:
-    std::vector<std::string> device_list;
-
-    cubeb* ctx{};
-    cubeb_stream* stream_backend{};
-    u32 num_channels{};
-
-    Common::RingBuffer<s16, 0x10000> queue;
-    std::array<s16, 2> last_frame{};
-    std::atomic<bool> should_flush{};
-
-    static long DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer,
-                             void* output_buffer, long num_frames);
-    static void StateCallback(cubeb_stream* stream, void* user_data, cubeb_state state);
-};
-
-CubebSink::CubebSink(std::string_view target_device_name) {
-    // Cubeb requires COM to be initialized on the thread calling cubeb_init on Windows
-#ifdef _WIN32
-    com_init_result = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
-#endif
-
-    if (cubeb_init(&ctx, "yuzu", nullptr) != CUBEB_OK) {
-        LOG_CRITICAL(Audio_Sink, "cubeb_init failed");
-        return;
-    }
-
-    if (target_device_name != auto_device_name && !target_device_name.empty()) {
-        cubeb_device_collection collection;
-        if (cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &collection) != CUBEB_OK) {
-            LOG_WARNING(Audio_Sink, "Audio output device enumeration not supported");
-        } else {
-            const auto collection_end{collection.device + collection.count};
-            const auto device{
-                std::find_if(collection.device, collection_end, [&](const cubeb_device_info& info) {
-                    return info.friendly_name != nullptr &&
-                           target_device_name == info.friendly_name;
-                })};
-            if (device != collection_end) {
-                output_device = device->devid;
-            }
-            cubeb_device_collection_destroy(ctx, &collection);
-        }
-    }
-}
-
-CubebSink::~CubebSink() {
-    if (!ctx) {
-        return;
-    }
-
-    for (auto& sink_stream : sink_streams) {
-        sink_stream.reset();
-    }
-
-    cubeb_destroy(ctx);
-
-#ifdef _WIN32
-    if (SUCCEEDED(com_init_result)) {
-        CoUninitialize();
-    }
-#endif
-}
-
-SinkStream& CubebSink::AcquireSinkStream(u32 sample_rate, u32 num_channels,
-                                         const std::string& name) {
-    sink_streams.push_back(
-        std::make_unique<CubebSinkStream>(ctx, sample_rate, num_channels, output_device, name));
-    return *sink_streams.back();
-}
-
-long CubebSinkStream::DataCallback([[maybe_unused]] cubeb_stream* stream, void* user_data,
-                                   [[maybe_unused]] const void* input_buffer, void* output_buffer,
-                                   long num_frames) {
-    auto* impl = static_cast<CubebSinkStream*>(user_data);
-    auto* buffer = static_cast<u8*>(output_buffer);
-
-    if (!impl) {
-        return {};
-    }
-
-    const std::size_t num_channels = impl->GetNumChannels();
-    const std::size_t samples_to_write = num_channels * num_frames;
-    const std::size_t samples_written = impl->queue.Pop(buffer, samples_to_write);
-
-    if (samples_written >= num_channels) {
-        std::memcpy(&impl->last_frame[0], buffer + (samples_written - num_channels) * sizeof(s16),
-                    num_channels * sizeof(s16));
-    }
-
-    // Fill the rest of the frames with last_frame
-    for (std::size_t i = samples_written; i < samples_to_write; i += num_channels) {
-        std::memcpy(buffer + i * sizeof(s16), &impl->last_frame[0], num_channels * sizeof(s16));
-    }
-
-    return num_frames;
-}
-
-void CubebSinkStream::StateCallback([[maybe_unused]] cubeb_stream* stream,
-                                    [[maybe_unused]] void* user_data,
-                                    [[maybe_unused]] cubeb_state state) {}
-
-std::vector<std::string> ListCubebSinkDevices() {
-    std::vector<std::string> device_list;
-    cubeb* ctx;
-
-    if (cubeb_init(&ctx, "yuzu Device Enumerator", nullptr) != CUBEB_OK) {
-        LOG_CRITICAL(Audio_Sink, "cubeb_init failed");
-        return {};
-    }
-
-    cubeb_device_collection collection;
-    if (cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &collection) != CUBEB_OK) {
-        LOG_WARNING(Audio_Sink, "Audio output device enumeration not supported");
-    } else {
-        for (std::size_t i = 0; i < collection.count; i++) {
-            const cubeb_device_info& device = collection.device[i];
-            if (device.friendly_name) {
-                device_list.emplace_back(device.friendly_name);
-            }
-        }
-        cubeb_device_collection_destroy(ctx, &collection);
-    }
-
-    cubeb_destroy(ctx);
-    return device_list;
-}
-
-} // namespace AudioCore
diff --git a/src/audio_core/cubeb_sink.h b/src/audio_core/cubeb_sink.h
deleted file mode 100644
index c124b7ee8b..0000000000
--- a/src/audio_core/cubeb_sink.h
+++ /dev/null
@@ -1,35 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include <string>
-#include <vector>
-
-#include <cubeb/cubeb.h>
-
-#include "audio_core/sink.h"
-
-namespace AudioCore {
-
-class CubebSink final : public Sink {
-public:
-    explicit CubebSink(std::string_view device_id);
-    ~CubebSink() override;
-
-    SinkStream& AcquireSinkStream(u32 sample_rate, u32 num_channels,
-                                  const std::string& name) override;
-
-private:
-    cubeb* ctx{};
-    cubeb_devid output_device{};
-    std::vector<SinkStreamPtr> sink_streams;
-
-#ifdef _WIN32
-    u32 com_init_result = 0;
-#endif
-};
-
-std::vector<std::string> ListCubebSinkDevices();
-
-} // namespace AudioCore
diff --git a/src/audio_core/delay_line.cpp b/src/audio_core/delay_line.cpp
deleted file mode 100644
index b1626a71be..0000000000
--- a/src/audio_core/delay_line.cpp
+++ /dev/null
@@ -1,107 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include <cstring>
-#include "audio_core/delay_line.h"
-
-namespace AudioCore {
-DelayLineBase::DelayLineBase() = default;
-DelayLineBase::~DelayLineBase() = default;
-
-void DelayLineBase::Initialize(s32 max_delay_, float* src_buffer) {
-    buffer = src_buffer;
-    buffer_end = buffer + max_delay_;
-    max_delay = max_delay_;
-    output = buffer;
-    SetDelay(max_delay_);
-    Clear();
-}
-
-void DelayLineBase::SetDelay(s32 new_delay) {
-    if (max_delay < new_delay) {
-        return;
-    }
-    delay = new_delay;
-    input = (buffer + ((output - buffer) + new_delay) % (max_delay + 1));
-}
-
-s32 DelayLineBase::GetDelay() const {
-    return delay;
-}
-
-s32 DelayLineBase::GetMaxDelay() const {
-    return max_delay;
-}
-
-f32 DelayLineBase::TapOut(s32 last_sample) {
-    const float* ptr = input - (last_sample + 1);
-    if (ptr < buffer) {
-        ptr += (max_delay + 1);
-    }
-
-    return *ptr;
-}
-
-f32 DelayLineBase::Tick(f32 sample) {
-    *(input++) = sample;
-    const auto out_sample = *(output++);
-
-    if (buffer_end < input) {
-        input = buffer;
-    }
-
-    if (buffer_end < output) {
-        output = buffer;
-    }
-
-    return out_sample;
-}
-
-float* DelayLineBase::GetInput() {
-    return input;
-}
-
-const float* DelayLineBase::GetInput() const {
-    return input;
-}
-
-f32 DelayLineBase::GetOutputSample() const {
-    return *output;
-}
-
-void DelayLineBase::Clear() {
-    std::memset(buffer, 0, sizeof(float) * max_delay);
-}
-
-void DelayLineBase::Reset() {
-    buffer = nullptr;
-    buffer_end = nullptr;
-    max_delay = 0;
-    input = nullptr;
-    output = nullptr;
-    delay = 0;
-}
-
-DelayLineAllPass::DelayLineAllPass() = default;
-DelayLineAllPass::~DelayLineAllPass() = default;
-
-void DelayLineAllPass::Initialize(u32 delay_, float coeffcient_, f32* src_buffer) {
-    DelayLineBase::Initialize(delay_, src_buffer);
-    SetCoefficient(coeffcient_);
-}
-
-void DelayLineAllPass::SetCoefficient(float coeffcient_) {
-    coefficient = coeffcient_;
-}
-
-f32 DelayLineAllPass::Tick(f32 sample) {
-    const auto temp = sample - coefficient * *output;
-    return coefficient * temp + DelayLineBase::Tick(temp);
-}
-
-void DelayLineAllPass::Reset() {
-    coefficient = 0.0f;
-    DelayLineBase::Reset();
-}
-
-} // namespace AudioCore
diff --git a/src/audio_core/delay_line.h b/src/audio_core/delay_line.h
deleted file mode 100644
index 05fda536f9..0000000000
--- a/src/audio_core/delay_line.h
+++ /dev/null
@@ -1,49 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2021 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include "common/common_types.h"
-
-namespace AudioCore {
-
-class DelayLineBase {
-public:
-    DelayLineBase();
-    ~DelayLineBase();
-
-    void Initialize(s32 max_delay_, float* src_buffer);
-    void SetDelay(s32 new_delay);
-    s32 GetDelay() const;
-    s32 GetMaxDelay() const;
-    f32 TapOut(s32 last_sample);
-    f32 Tick(f32 sample);
-    float* GetInput();
-    const float* GetInput() const;
-    f32 GetOutputSample() const;
-    void Clear();
-    void Reset();
-
-protected:
-    float* buffer{nullptr};
-    float* buffer_end{nullptr};
-    s32 max_delay{};
-    float* input{nullptr};
-    float* output{nullptr};
-    s32 delay{};
-};
-
-class DelayLineAllPass final : public DelayLineBase {
-public:
-    DelayLineAllPass();
-    ~DelayLineAllPass();
-
-    void Initialize(u32 delay, float coeffcient_, f32* src_buffer);
-    void SetCoefficient(float coeffcient_);
-    f32 Tick(f32 sample);
-    void Reset();
-
-private:
-    float coefficient{};
-};
-} // namespace AudioCore
diff --git a/src/audio_core/device/audio_buffer.h b/src/audio_core/device/audio_buffer.h
new file mode 100644
index 0000000000..cae7fa9704
--- /dev/null
+++ b/src/audio_core/device/audio_buffer.h
@@ -0,0 +1,21 @@
+// 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 completed playing.
+    s64 played_timestamp;
+    /// Game memory address for these samples.
+    VAddr samples;
+    /// Unqiue identifier for this buffer.
+    u64 tag;
+    /// Size of the samples buffer.
+    u64 size;
+};
+
+} // namespace AudioCore
diff --git a/src/audio_core/device/audio_buffers.h b/src/audio_core/device/audio_buffers.h
new file mode 100644
index 0000000000..5d1979ea0c
--- /dev/null
+++ b/src/audio_core/device/audio_buffers.h
@@ -0,0 +1,304 @@
+// 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 "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(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(std::vector<AudioBuffer>& 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 timestamp - The released timestamp for this buffer.
+     * @return Is the buffer was released.
+     */
+    bool ReleaseBuffers(Core::Timing::CoreTiming& core_timing, DeviceSession& session) {
+        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 (!session.IsBufferConsumed(buffers[index].tag)) {
+                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;
+            }
+
+            tags[released++] = tag;
+
+            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(std::vector<AudioBuffer>& 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};
+        std::vector<AudioBuffer> 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;
+    }
+
+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
diff --git a/src/audio_core/device/device_session.cpp b/src/audio_core/device/device_session.cpp
new file mode 100644
index 0000000000..095fc96ce1
--- /dev/null
+++ b/src/audio_core/device/device_session.cpp
@@ -0,0 +1,114 @@
+// 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/memory.h"
+
+namespace AudioCore {
+
+DeviceSession::DeviceSession(Core::System& system_) : system{system_} {}
+
+DeviceSession::~DeviceSession() {
+    Finalize();
+}
+
+Result DeviceSession::Initialize(std::string_view name_, SampleFormat sample_format_,
+                                 u16 channel_count_, size_t session_id_, u32 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_;
+    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;
+    }
+}
+
+void DeviceSession::Start() {
+    stream->SetPlayedSampleCount(played_sample_count);
+    stream->Start();
+}
+
+void DeviceSession::Stop() {
+    if (stream) {
+        played_sample_count = stream->GetPlayedSampleCount();
+        stream->Stop();
+    }
+}
+
+void DeviceSession::AppendBuffers(std::span<AudioBuffer> buffers) const {
+    auto& memory{system.Memory()};
+
+    for (size_t i = 0; i < buffers.size(); i++) {
+        Sink::SinkBuffer new_buffer{
+            .frames = buffers[i].size / (channel_count * sizeof(s16)),
+            .frames_played = 0,
+            .tag = buffers[i].tag,
+            .consumed = false,
+        };
+
+        if (type == Sink::StreamType::In) {
+            std::vector<s16> samples{};
+            stream->AppendBuffer(new_buffer, samples);
+        } else {
+            std::vector<s16> samples(buffers[i].size / sizeof(s16));
+            memory.ReadBlockUnsafe(buffers[i].samples, samples.data(), buffers[i].size);
+            stream->AppendBuffer(new_buffer, samples);
+        }
+    }
+}
+
+void DeviceSession::ReleaseBuffer(AudioBuffer& buffer) const {
+    if (type == Sink::StreamType::In) {
+        auto& memory{system.Memory()};
+        auto samples{stream->ReleaseBuffer(buffer.size / sizeof(s16))};
+        memory.WriteBlockUnsafe(buffer.samples, samples.data(), buffer.size);
+    }
+}
+
+bool DeviceSession::IsBufferConsumed(u64 tag) const {
+    if (stream) {
+        return stream->IsBufferConsumed(tag);
+    }
+    return true;
+}
+
+void DeviceSession::SetVolume(f32 volume) const {
+    if (stream) {
+        stream->SetSystemVolume(volume);
+    }
+}
+
+u64 DeviceSession::GetPlayedSampleCount() const {
+    if (stream) {
+        return stream->GetPlayedSampleCount();
+    }
+    return 0;
+}
+
+} // namespace AudioCore
diff --git a/src/audio_core/device/device_session.h b/src/audio_core/device/device_session.h
new file mode 100644
index 0000000000..4a031b7651
--- /dev/null
+++ b/src/audio_core/device/device_session.h
@@ -0,0 +1,126 @@
+// 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 "audio_core/sink/sink.h"
+#include "core/hle/service/audio/errors.h"
+
+namespace Core {
+class System;
+}
+
+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                  - Handle for this device session (unused).
+     * @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, u32 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<AudioBuffer> buffers) const;
+
+    /**
+     * (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(AudioBuffer& buffer) const;
+
+    /**
+     * Check if the buffer for the given tag has been consumed by the backend.
+     *
+     * @param tag - Unqiue tag of the buffer to check.
+     * @return true if the buffer has been consumed, otherwise false.
+     */
+    bool IsBufferConsumed(u64 tag) const;
+
+    /**
+     * Start this device session, starting the backend stream.
+     */
+    void Start();
+
+    /**
+     * Stop this device session, stopping the backend stream.
+     */
+    void Stop();
+
+    /**
+     * 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;
+
+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{};
+    /// Handle of this device session
+    u32 handle{};
+    /// Applet resource user id of this device session
+    u64 applet_resource_user_id{};
+    /// Total number of samples played by this device session
+    u64 played_sample_count{};
+    /// Is this session initialised?
+    bool initialized{};
+};
+
+} // namespace AudioCore
diff --git a/src/audio_core/effect_context.cpp b/src/audio_core/effect_context.cpp
deleted file mode 100644
index 79bcd11923..0000000000
--- a/src/audio_core/effect_context.cpp
+++ /dev/null
@@ -1,320 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include <algorithm>
-#include "audio_core/effect_context.h"
-
-namespace AudioCore {
-namespace {
-bool ValidChannelCountForEffect(s32 channel_count) {
-    return channel_count == 1 || channel_count == 2 || channel_count == 4 || channel_count == 6;
-}
-} // namespace
-
-EffectContext::EffectContext(std::size_t effect_count_) : effect_count(effect_count_) {
-    effects.reserve(effect_count);
-    std::generate_n(std::back_inserter(effects), effect_count,
-                    [] { return std::make_unique<EffectStubbed>(); });
-}
-EffectContext::~EffectContext() = default;
-
-std::size_t EffectContext::GetCount() const {
-    return effect_count;
-}
-
-EffectBase* EffectContext::GetInfo(std::size_t i) {
-    return effects.at(i).get();
-}
-
-EffectBase* EffectContext::RetargetEffect(std::size_t i, EffectType effect) {
-    switch (effect) {
-    case EffectType::Invalid:
-        effects[i] = std::make_unique<EffectStubbed>();
-        break;
-    case EffectType::BufferMixer:
-        effects[i] = std::make_unique<EffectBufferMixer>();
-        break;
-    case EffectType::Aux:
-        effects[i] = std::make_unique<EffectAuxInfo>();
-        break;
-    case EffectType::Delay:
-        effects[i] = std::make_unique<EffectDelay>();
-        break;
-    case EffectType::Reverb:
-        effects[i] = std::make_unique<EffectReverb>();
-        break;
-    case EffectType::I3dl2Reverb:
-        effects[i] = std::make_unique<EffectI3dl2Reverb>();
-        break;
-    case EffectType::BiquadFilter:
-        effects[i] = std::make_unique<EffectBiquadFilter>();
-        break;
-    default:
-        ASSERT_MSG(false, "Unimplemented effect {}", effect);
-        effects[i] = std::make_unique<EffectStubbed>();
-    }
-    return GetInfo(i);
-}
-
-const EffectBase* EffectContext::GetInfo(std::size_t i) const {
-    return effects.at(i).get();
-}
-
-EffectStubbed::EffectStubbed() : EffectBase(EffectType::Invalid) {}
-EffectStubbed::~EffectStubbed() = default;
-
-void EffectStubbed::Update([[maybe_unused]] EffectInfo::InParams& in_params) {}
-void EffectStubbed::UpdateForCommandGeneration() {}
-
-EffectBase::EffectBase(EffectType effect_type_) : effect_type(effect_type_) {}
-EffectBase::~EffectBase() = default;
-
-UsageState EffectBase::GetUsage() const {
-    return usage;
-}
-
-EffectType EffectBase::GetType() const {
-    return effect_type;
-}
-
-bool EffectBase::IsEnabled() const {
-    return enabled;
-}
-
-s32 EffectBase::GetMixID() const {
-    return mix_id;
-}
-
-s32 EffectBase::GetProcessingOrder() const {
-    return processing_order;
-}
-
-std::vector<u8>& EffectBase::GetWorkBuffer() {
-    return work_buffer;
-}
-
-const std::vector<u8>& EffectBase::GetWorkBuffer() const {
-    return work_buffer;
-}
-
-EffectI3dl2Reverb::EffectI3dl2Reverb() : EffectGeneric(EffectType::I3dl2Reverb) {}
-EffectI3dl2Reverb::~EffectI3dl2Reverb() = default;
-
-void EffectI3dl2Reverb::Update(EffectInfo::InParams& in_params) {
-    auto& params = GetParams();
-    const auto* reverb_params = reinterpret_cast<I3dl2ReverbParams*>(in_params.raw.data());
-    if (!ValidChannelCountForEffect(reverb_params->max_channels)) {
-        ASSERT_MSG(false, "Invalid reverb max channel count {}", reverb_params->max_channels);
-        return;
-    }
-
-    const auto last_status = params.status;
-    mix_id = in_params.mix_id;
-    processing_order = in_params.processing_order;
-    params = *reverb_params;
-    if (!ValidChannelCountForEffect(reverb_params->channel_count)) {
-        params.channel_count = params.max_channels;
-    }
-    enabled = in_params.is_enabled;
-    if (last_status != ParameterStatus::Updated) {
-        params.status = last_status;
-    }
-
-    if (in_params.is_new || skipped) {
-        usage = UsageState::Initialized;
-        params.status = ParameterStatus::Initialized;
-        skipped = in_params.buffer_address == 0 || in_params.buffer_size == 0;
-        if (!skipped) {
-            auto& cur_work_buffer = GetWorkBuffer();
-            // Has two buffers internally
-            cur_work_buffer.resize(in_params.buffer_size * 2);
-            std::fill(cur_work_buffer.begin(), cur_work_buffer.end(), 0);
-        }
-    }
-}
-
-void EffectI3dl2Reverb::UpdateForCommandGeneration() {
-    if (enabled) {
-        usage = UsageState::Running;
-    } else {
-        usage = UsageState::Stopped;
-    }
-    GetParams().status = ParameterStatus::Updated;
-}
-
-I3dl2ReverbState& EffectI3dl2Reverb::GetState() {
-    return state;
-}
-
-const I3dl2ReverbState& EffectI3dl2Reverb::GetState() const {
-    return state;
-}
-
-EffectBiquadFilter::EffectBiquadFilter() : EffectGeneric(EffectType::BiquadFilter) {}
-EffectBiquadFilter::~EffectBiquadFilter() = default;
-
-void EffectBiquadFilter::Update(EffectInfo::InParams& in_params) {
-    auto& params = GetParams();
-    const auto* biquad_params = reinterpret_cast<BiquadFilterParams*>(in_params.raw.data());
-    mix_id = in_params.mix_id;
-    processing_order = in_params.processing_order;
-    params = *biquad_params;
-    enabled = in_params.is_enabled;
-}
-
-void EffectBiquadFilter::UpdateForCommandGeneration() {
-    if (enabled) {
-        usage = UsageState::Running;
-    } else {
-        usage = UsageState::Stopped;
-    }
-    GetParams().status = ParameterStatus::Updated;
-}
-
-EffectAuxInfo::EffectAuxInfo() : EffectGeneric(EffectType::Aux) {}
-EffectAuxInfo::~EffectAuxInfo() = default;
-
-void EffectAuxInfo::Update(EffectInfo::InParams& in_params) {
-    const auto* aux_params = reinterpret_cast<AuxInfo*>(in_params.raw.data());
-    mix_id = in_params.mix_id;
-    processing_order = in_params.processing_order;
-    GetParams() = *aux_params;
-    enabled = in_params.is_enabled;
-
-    if (in_params.is_new || skipped) {
-        skipped = aux_params->send_buffer_info == 0 || aux_params->return_buffer_info == 0;
-        if (skipped) {
-            return;
-        }
-
-        // There's two AuxInfos which are an identical size, the first one is managed by the cpu,
-        // the second is managed by the dsp. All we care about is managing the DSP one
-        send_info = aux_params->send_buffer_info + sizeof(AuxInfoDSP);
-        send_buffer = aux_params->send_buffer_info + (sizeof(AuxInfoDSP) * 2);
-
-        recv_info = aux_params->return_buffer_info + sizeof(AuxInfoDSP);
-        recv_buffer = aux_params->return_buffer_info + (sizeof(AuxInfoDSP) * 2);
-    }
-}
-
-void EffectAuxInfo::UpdateForCommandGeneration() {
-    if (enabled) {
-        usage = UsageState::Running;
-    } else {
-        usage = UsageState::Stopped;
-    }
-}
-
-VAddr EffectAuxInfo::GetSendInfo() const {
-    return send_info;
-}
-
-VAddr EffectAuxInfo::GetSendBuffer() const {
-    return send_buffer;
-}
-
-VAddr EffectAuxInfo::GetRecvInfo() const {
-    return recv_info;
-}
-
-VAddr EffectAuxInfo::GetRecvBuffer() const {
-    return recv_buffer;
-}
-
-EffectDelay::EffectDelay() : EffectGeneric(EffectType::Delay) {}
-EffectDelay::~EffectDelay() = default;
-
-void EffectDelay::Update(EffectInfo::InParams& in_params) {
-    const auto* delay_params = reinterpret_cast<DelayParams*>(in_params.raw.data());
-    auto& params = GetParams();
-    if (!ValidChannelCountForEffect(delay_params->max_channels)) {
-        return;
-    }
-
-    const auto last_status = params.status;
-    mix_id = in_params.mix_id;
-    processing_order = in_params.processing_order;
-    params = *delay_params;
-    if (!ValidChannelCountForEffect(delay_params->channels)) {
-        params.channels = params.max_channels;
-    }
-    enabled = in_params.is_enabled;
-
-    if (last_status != ParameterStatus::Updated) {
-        params.status = last_status;
-    }
-
-    if (in_params.is_new || skipped) {
-        usage = UsageState::Initialized;
-        params.status = ParameterStatus::Initialized;
-        skipped = in_params.buffer_address == 0 || in_params.buffer_size == 0;
-    }
-}
-
-void EffectDelay::UpdateForCommandGeneration() {
-    if (enabled) {
-        usage = UsageState::Running;
-    } else {
-        usage = UsageState::Stopped;
-    }
-    GetParams().status = ParameterStatus::Updated;
-}
-
-EffectBufferMixer::EffectBufferMixer() : EffectGeneric(EffectType::BufferMixer) {}
-EffectBufferMixer::~EffectBufferMixer() = default;
-
-void EffectBufferMixer::Update(EffectInfo::InParams& in_params) {
-    mix_id = in_params.mix_id;
-    processing_order = in_params.processing_order;
-    GetParams() = *reinterpret_cast<BufferMixerParams*>(in_params.raw.data());
-    enabled = in_params.is_enabled;
-}
-
-void EffectBufferMixer::UpdateForCommandGeneration() {
-    if (enabled) {
-        usage = UsageState::Running;
-    } else {
-        usage = UsageState::Stopped;
-    }
-}
-
-EffectReverb::EffectReverb() : EffectGeneric(EffectType::Reverb) {}
-EffectReverb::~EffectReverb() = default;
-
-void EffectReverb::Update(EffectInfo::InParams& in_params) {
-    const auto* reverb_params = reinterpret_cast<ReverbParams*>(in_params.raw.data());
-    auto& params = GetParams();
-    if (!ValidChannelCountForEffect(reverb_params->max_channels)) {
-        return;
-    }
-
-    const auto last_status = params.status;
-    mix_id = in_params.mix_id;
-    processing_order = in_params.processing_order;
-    params = *reverb_params;
-    if (!ValidChannelCountForEffect(reverb_params->channels)) {
-        params.channels = params.max_channels;
-    }
-    enabled = in_params.is_enabled;
-
-    if (last_status != ParameterStatus::Updated) {
-        params.status = last_status;
-    }
-
-    if (in_params.is_new || skipped) {
-        usage = UsageState::Initialized;
-        params.status = ParameterStatus::Initialized;
-        skipped = in_params.buffer_address == 0 || in_params.buffer_size == 0;
-    }
-}
-
-void EffectReverb::UpdateForCommandGeneration() {
-    if (enabled) {
-        usage = UsageState::Running;
-    } else {
-        usage = UsageState::Stopped;
-    }
-    GetParams().status = ParameterStatus::Updated;
-}
-
-} // namespace AudioCore
diff --git a/src/audio_core/effect_context.h b/src/audio_core/effect_context.h
deleted file mode 100644
index cb47df4729..0000000000
--- a/src/audio_core/effect_context.h
+++ /dev/null
@@ -1,349 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include <array>
-#include <memory>
-#include <vector>
-#include "audio_core/common.h"
-#include "audio_core/delay_line.h"
-#include "common/common_funcs.h"
-#include "common/common_types.h"
-#include "common/swap.h"
-
-namespace AudioCore {
-enum class EffectType : u8 {
-    Invalid = 0,
-    BufferMixer = 1,
-    Aux = 2,
-    Delay = 3,
-    Reverb = 4,
-    I3dl2Reverb = 5,
-    BiquadFilter = 6,
-};
-
-enum class UsageStatus : u8 {
-    Invalid = 0,
-    New = 1,
-    Initialized = 2,
-    Used = 3,
-    Removed = 4,
-};
-
-enum class UsageState {
-    Invalid = 0,
-    Initialized = 1,
-    Running = 2,
-    Stopped = 3,
-};
-
-enum class ParameterStatus : u8 {
-    Initialized = 0,
-    Updating = 1,
-    Updated = 2,
-};
-
-struct BufferMixerParams {
-    std::array<s8, AudioCommon::MAX_MIX_BUFFERS> input{};
-    std::array<s8, AudioCommon::MAX_MIX_BUFFERS> output{};
-    std::array<float_le, AudioCommon::MAX_MIX_BUFFERS> volume{};
-    s32_le count{};
-};
-static_assert(sizeof(BufferMixerParams) == 0x94, "BufferMixerParams is an invalid size");
-
-struct AuxInfoDSP {
-    u32_le read_offset{};
-    u32_le write_offset{};
-    u32_le remaining{};
-    INSERT_PADDING_WORDS(13);
-};
-static_assert(sizeof(AuxInfoDSP) == 0x40, "AuxInfoDSP is an invalid size");
-
-struct AuxInfo {
-    std::array<s8, AudioCommon::MAX_MIX_BUFFERS> input_mix_buffers{};
-    std::array<s8, AudioCommon::MAX_MIX_BUFFERS> output_mix_buffers{};
-    u32_le count{};
-    s32_le sample_rate{};
-    s32_le sample_count{};
-    s32_le mix_buffer_count{};
-    u64_le send_buffer_info{};
-    u64_le send_buffer_base{};
-
-    u64_le return_buffer_info{};
-    u64_le return_buffer_base{};
-};
-static_assert(sizeof(AuxInfo) == 0x60, "AuxInfo is an invalid size");
-
-struct I3dl2ReverbParams {
-    std::array<s8, AudioCommon::MAX_CHANNEL_COUNT> input{};
-    std::array<s8, AudioCommon::MAX_CHANNEL_COUNT> output{};
-    u16_le max_channels{};
-    u16_le channel_count{};
-    INSERT_PADDING_BYTES(1);
-    u32_le sample_rate{};
-    f32 room_hf{};
-    f32 hf_reference{};
-    f32 decay_time{};
-    f32 hf_decay_ratio{};
-    f32 room{};
-    f32 reflection{};
-    f32 reverb{};
-    f32 diffusion{};
-    f32 reflection_delay{};
-    f32 reverb_delay{};
-    f32 density{};
-    f32 dry_gain{};
-    ParameterStatus status{};
-    INSERT_PADDING_BYTES(3);
-};
-static_assert(sizeof(I3dl2ReverbParams) == 0x4c, "I3dl2ReverbParams is an invalid size");
-
-struct BiquadFilterParams {
-    std::array<s8, AudioCommon::MAX_CHANNEL_COUNT> input{};
-    std::array<s8, AudioCommon::MAX_CHANNEL_COUNT> output{};
-    std::array<s16_le, 3> numerator;
-    std::array<s16_le, 2> denominator;
-    s8 channel_count{};
-    ParameterStatus status{};
-};
-static_assert(sizeof(BiquadFilterParams) == 0x18, "BiquadFilterParams is an invalid size");
-
-struct DelayParams {
-    std::array<s8, AudioCommon::MAX_CHANNEL_COUNT> input{};
-    std::array<s8, AudioCommon::MAX_CHANNEL_COUNT> output{};
-    u16_le max_channels{};
-    u16_le channels{};
-    s32_le max_delay{};
-    s32_le delay{};
-    s32_le sample_rate{};
-    s32_le gain{};
-    s32_le feedback_gain{};
-    s32_le out_gain{};
-    s32_le dry_gain{};
-    s32_le channel_spread{};
-    s32_le low_pass{};
-    ParameterStatus status{};
-    INSERT_PADDING_BYTES(3);
-};
-static_assert(sizeof(DelayParams) == 0x38, "DelayParams is an invalid size");
-
-struct ReverbParams {
-    std::array<s8, AudioCommon::MAX_CHANNEL_COUNT> input{};
-    std::array<s8, AudioCommon::MAX_CHANNEL_COUNT> output{};
-    u16_le max_channels{};
-    u16_le channels{};
-    s32_le sample_rate{};
-    s32_le mode0{};
-    s32_le mode0_gain{};
-    s32_le pre_delay{};
-    s32_le mode1{};
-    s32_le mode1_gain{};
-    s32_le decay{};
-    s32_le hf_decay_ratio{};
-    s32_le coloration{};
-    s32_le reverb_gain{};
-    s32_le out_gain{};
-    s32_le dry_gain{};
-    ParameterStatus status{};
-    INSERT_PADDING_BYTES(3);
-};
-static_assert(sizeof(ReverbParams) == 0x44, "ReverbParams is an invalid size");
-
-class EffectInfo {
-public:
-    struct InParams {
-        EffectType type{};
-        u8 is_new{};
-        u8 is_enabled{};
-        INSERT_PADDING_BYTES(1);
-        s32_le mix_id{};
-        u64_le buffer_address{};
-        u64_le buffer_size{};
-        s32_le processing_order{};
-        INSERT_PADDING_BYTES(4);
-        union {
-            std::array<u8, 0xa0> raw;
-        };
-    };
-    static_assert(sizeof(InParams) == 0xc0, "InParams is an invalid size");
-
-    struct OutParams {
-        UsageStatus status{};
-        INSERT_PADDING_BYTES(15);
-    };
-    static_assert(sizeof(OutParams) == 0x10, "OutParams is an invalid size");
-};
-
-struct AuxAddress {
-    VAddr send_dsp_info{};
-    VAddr send_buffer_base{};
-    VAddr return_dsp_info{};
-    VAddr return_buffer_base{};
-};
-
-class EffectBase {
-public:
-    explicit EffectBase(EffectType effect_type_);
-    virtual ~EffectBase();
-
-    virtual void Update(EffectInfo::InParams& in_params) = 0;
-    virtual void UpdateForCommandGeneration() = 0;
-    [[nodiscard]] UsageState GetUsage() const;
-    [[nodiscard]] EffectType GetType() const;
-    [[nodiscard]] bool IsEnabled() const;
-    [[nodiscard]] s32 GetMixID() const;
-    [[nodiscard]] s32 GetProcessingOrder() const;
-    [[nodiscard]] std::vector<u8>& GetWorkBuffer();
-    [[nodiscard]] const std::vector<u8>& GetWorkBuffer() const;
-
-protected:
-    UsageState usage{UsageState::Invalid};
-    EffectType effect_type{};
-    s32 mix_id{};
-    s32 processing_order{};
-    bool enabled = false;
-    std::vector<u8> work_buffer{};
-};
-
-template <typename T>
-class EffectGeneric : public EffectBase {
-public:
-    explicit EffectGeneric(EffectType effect_type_) : EffectBase(effect_type_) {}
-
-    T& GetParams() {
-        return internal_params;
-    }
-
-    const T& GetParams() const {
-        return internal_params;
-    }
-
-private:
-    T internal_params{};
-};
-
-class EffectStubbed : public EffectBase {
-public:
-    explicit EffectStubbed();
-    ~EffectStubbed() override;
-
-    void Update(EffectInfo::InParams& in_params) override;
-    void UpdateForCommandGeneration() override;
-};
-
-struct I3dl2ReverbState {
-    f32 lowpass_0{};
-    f32 lowpass_1{};
-    f32 lowpass_2{};
-
-    DelayLineBase early_delay_line{};
-    std::array<u32, AudioCommon::I3DL2REVERB_TAPS> early_tap_steps{};
-    f32 early_gain{};
-    f32 late_gain{};
-
-    u32 early_to_late_taps{};
-    std::array<DelayLineBase, AudioCommon::I3DL2REVERB_DELAY_LINE_COUNT> fdn_delay_line{};
-    std::array<DelayLineAllPass, AudioCommon::I3DL2REVERB_DELAY_LINE_COUNT> decay_delay_line0{};
-    std::array<DelayLineAllPass, AudioCommon::I3DL2REVERB_DELAY_LINE_COUNT> decay_delay_line1{};
-    f32 last_reverb_echo{};
-    DelayLineBase center_delay_line{};
-    std::array<std::array<f32, AudioCommon::I3DL2REVERB_DELAY_LINE_COUNT>, 3> lpf_coefficients{};
-    std::array<f32, AudioCommon::I3DL2REVERB_DELAY_LINE_COUNT> shelf_filter{};
-    f32 dry_gain{};
-};
-
-class EffectI3dl2Reverb : public EffectGeneric<I3dl2ReverbParams> {
-public:
-    explicit EffectI3dl2Reverb();
-    ~EffectI3dl2Reverb() override;
-
-    void Update(EffectInfo::InParams& in_params) override;
-    void UpdateForCommandGeneration() override;
-
-    I3dl2ReverbState& GetState();
-    const I3dl2ReverbState& GetState() const;
-
-private:
-    bool skipped = false;
-    I3dl2ReverbState state{};
-};
-
-class EffectBiquadFilter : public EffectGeneric<BiquadFilterParams> {
-public:
-    explicit EffectBiquadFilter();
-    ~EffectBiquadFilter() override;
-
-    void Update(EffectInfo::InParams& in_params) override;
-    void UpdateForCommandGeneration() override;
-};
-
-class EffectAuxInfo : public EffectGeneric<AuxInfo> {
-public:
-    explicit EffectAuxInfo();
-    ~EffectAuxInfo() override;
-
-    void Update(EffectInfo::InParams& in_params) override;
-    void UpdateForCommandGeneration() override;
-    [[nodiscard]] VAddr GetSendInfo() const;
-    [[nodiscard]] VAddr GetSendBuffer() const;
-    [[nodiscard]] VAddr GetRecvInfo() const;
-    [[nodiscard]] VAddr GetRecvBuffer() const;
-
-private:
-    VAddr send_info{};
-    VAddr send_buffer{};
-    VAddr recv_info{};
-    VAddr recv_buffer{};
-    bool skipped = false;
-    AuxAddress addresses{};
-};
-
-class EffectDelay : public EffectGeneric<DelayParams> {
-public:
-    explicit EffectDelay();
-    ~EffectDelay() override;
-
-    void Update(EffectInfo::InParams& in_params) override;
-    void UpdateForCommandGeneration() override;
-
-private:
-    bool skipped = false;
-};
-
-class EffectBufferMixer : public EffectGeneric<BufferMixerParams> {
-public:
-    explicit EffectBufferMixer();
-    ~EffectBufferMixer() override;
-
-    void Update(EffectInfo::InParams& in_params) override;
-    void UpdateForCommandGeneration() override;
-};
-
-class EffectReverb : public EffectGeneric<ReverbParams> {
-public:
-    explicit EffectReverb();
-    ~EffectReverb() override;
-
-    void Update(EffectInfo::InParams& in_params) override;
-    void UpdateForCommandGeneration() override;
-
-private:
-    bool skipped = false;
-};
-
-class EffectContext {
-public:
-    explicit EffectContext(std::size_t effect_count_);
-    ~EffectContext();
-
-    [[nodiscard]] std::size_t GetCount() const;
-    [[nodiscard]] EffectBase* GetInfo(std::size_t i);
-    [[nodiscard]] EffectBase* RetargetEffect(std::size_t i, EffectType effect);
-    [[nodiscard]] const EffectBase* GetInfo(std::size_t i) const;
-
-private:
-    std::size_t effect_count{};
-    std::vector<std::unique_ptr<EffectBase>> effects;
-};
-} // namespace AudioCore
diff --git a/src/audio_core/in/audio_in.cpp b/src/audio_core/in/audio_in.cpp
new file mode 100644
index 0000000000..c946895d64
--- /dev/null
+++ b/src/audio_core/in/audio_in.cpp
@@ -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::ERR_BUFFER_COUNT_EXCEEDED;
+}
+
+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() {
+    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) {
+    std::scoped_lock l{parent_mutex};
+    return system.ContainsAudioBuffer(tag);
+}
+
+u32 In::GetBufferCount() {
+    std::scoped_lock l{parent_mutex};
+    return system.GetBufferCount();
+}
+
+u64 In::GetPlayedSampleCount() {
+    std::scoped_lock l{parent_mutex};
+    return system.GetPlayedSampleCount();
+}
+
+} // namespace AudioCore::AudioIn
diff --git a/src/audio_core/in/audio_in.h b/src/audio_core/in/audio_in.h
new file mode 100644
index 0000000000..6253891d5e
--- /dev/null
+++ b/src/audio_core/in/audio_in.h
@@ -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();
+
+    /**
+     * 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);
+
+    /**
+     * Get the maximum number of buffers.
+     *
+     * @return The maximum number of buffers.
+     */
+    u32 GetBufferCount();
+
+    /**
+     * Get the total played sample count for this audio in.
+     *
+     * @return The played sample count.
+     */
+    u64 GetPlayedSampleCount();
+
+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
diff --git a/src/audio_core/in/audio_in_system.cpp b/src/audio_core/in/audio_in_system.cpp
new file mode 100644
index 0000000000..ec5d37ed44
--- /dev/null
+++ b/src/audio_core/in/audio_in_system.cpp
@@ -0,0 +1,213 @@
+// 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();
+    buffer_event->GetWritableEvent().Signal();
+}
+
+void System::StartSession() {
+    session->Start();
+}
+
+size_t System::GetSessionId() const {
+    return session_id;
+}
+
+std::string_view System::GetDefaultDeviceName() {
+    return "BuiltInHeadset";
+}
+
+std::string_view System::GetDefaultUacDeviceName() {
+    return "Uac";
+}
+
+Result System::IsConfigValid(const std::string_view device_name,
+                             const AudioInParameter& in_params) {
+    if ((device_name.size() > 0) &&
+        (device_name != GetDefaultDeviceName() && device_name != GetDefaultUacDeviceName())) {
+        return Service::Audio::ERR_INVALID_DEVICE_NAME;
+    }
+
+    if (in_params.sample_rate != TargetSampleRate && in_params.sample_rate > 0) {
+        return Service::Audio::ERR_INVALID_SAMPLE_RATE;
+    }
+
+    return ResultSuccess;
+}
+
+Result System::Initialize(std::string& device_name, const AudioInParameter& in_params,
+                          const u32 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::ERR_OPERATION_FAILED;
+    }
+
+    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;
+
+    std::vector<AudioBuffer> buffers_to_flush{};
+    buffers.RegisterBuffers(buffers_to_flush);
+    session->AppendBuffers(buffers_to_flush);
+
+    return ResultSuccess;
+}
+
+Result System::Stop() {
+    if (state == State::Started) {
+        session->Stop();
+        session->SetVolume(0.0f);
+        state = State::Stopped;
+    }
+
+    return ResultSuccess;
+}
+
+bool System::AppendBuffer(const AudioInBuffer& buffer, const u64 tag) {
+    if (buffers.GetTotalBufferCount() == BufferCount) {
+        return false;
+    }
+
+    AudioBuffer new_buffer{
+        .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) {
+        std::vector<AudioBuffer> registered_buffers{};
+        buffers.RegisterBuffers(registered_buffers);
+        session->AppendBuffers(registered_buffers);
+    }
+}
+
+void System::ReleaseBuffers() {
+    bool signal{buffers.ReleaseBuffers(system.CoreTiming(), *session)};
+
+    if (signal) {
+        // Signal if any buffer was released, or if none are registered, we need more.
+        buffer_event->GetWritableEvent().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->GetWritableEvent().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) {
+    return buffers.ContainsBuffer(tag);
+}
+
+u32 System::GetBufferCount() {
+    return buffers.GetAppendedRegisteredCount();
+}
+
+u64 System::GetPlayedSampleCount() const {
+    return session->GetPlayedSampleCount();
+}
+
+bool System::IsUac() const {
+    return is_uac;
+}
+
+} // namespace AudioCore::AudioIn
diff --git a/src/audio_core/in/audio_in_system.h b/src/audio_core/in/audio_in_system.h
new file mode 100644
index 0000000000..165e35d83b
--- /dev/null
+++ b/src/audio_core/in/audio_in_system.h
@@ -0,0 +1,275 @@
+// 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;
+}
+
+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();
+
+    /**
+     * 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();
+
+    /**
+     * 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);
+
+    /**
+     * Initialize this system.
+     *
+     * @param device_name             - The name of the requested input device.
+     * @param in_params               - Input parameters, see AudioInParameter.
+     * @param handle                  - Unused.
+     * @param applet_resource_user_id - Unused.
+     * @return Result code.
+     */
+    Result Initialize(std::string& device_name, const AudioInParameter& in_params, u32 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 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);
+
+    /**
+     * Get the maximum number of usable buffers (default 32).
+     *
+     * @return The number of buffers.
+     */
+    u32 GetBufferCount();
+
+    /**
+     * 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;
+    /// (Unused)
+    u32 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
diff --git a/src/audio_core/info_updater.cpp b/src/audio_core/info_updater.cpp
deleted file mode 100644
index 0065e6e536..0000000000
--- a/src/audio_core/info_updater.cpp
+++ /dev/null
@@ -1,511 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include "audio_core/behavior_info.h"
-#include "audio_core/effect_context.h"
-#include "audio_core/info_updater.h"
-#include "audio_core/memory_pool.h"
-#include "audio_core/mix_context.h"
-#include "audio_core/sink_context.h"
-#include "audio_core/splitter_context.h"
-#include "audio_core/voice_context.h"
-#include "common/logging/log.h"
-
-namespace AudioCore {
-
-InfoUpdater::InfoUpdater(const std::vector<u8>& in_params_, std::vector<u8>& out_params_,
-                         BehaviorInfo& behavior_info_)
-    : in_params(in_params_), out_params(out_params_), behavior_info(behavior_info_) {
-    ASSERT(
-        AudioCommon::CanConsumeBuffer(in_params.size(), 0, sizeof(AudioCommon::UpdateDataHeader)));
-    std::memcpy(&input_header, in_params.data(), sizeof(AudioCommon::UpdateDataHeader));
-    output_header.total_size = sizeof(AudioCommon::UpdateDataHeader);
-}
-
-InfoUpdater::~InfoUpdater() = default;
-
-bool InfoUpdater::UpdateBehaviorInfo(BehaviorInfo& in_behavior_info) {
-    if (input_header.size.behavior != sizeof(BehaviorInfo::InParams)) {
-        LOG_ERROR(Audio, "Behavior info is an invalid size, expecting 0x{:X} but got 0x{:X}",
-                  sizeof(BehaviorInfo::InParams), input_header.size.behavior);
-        return false;
-    }
-
-    if (!AudioCommon::CanConsumeBuffer(in_params.size(), input_offset,
-                                       sizeof(BehaviorInfo::InParams))) {
-        LOG_ERROR(Audio, "Buffer is an invalid size!");
-        return false;
-    }
-
-    BehaviorInfo::InParams behavior_in{};
-    std::memcpy(&behavior_in, in_params.data() + input_offset, sizeof(BehaviorInfo::InParams));
-    input_offset += sizeof(BehaviorInfo::InParams);
-
-    // Make sure it's an audio revision we can actually support
-    if (!AudioCommon::IsValidRevision(behavior_in.revision)) {
-        LOG_ERROR(Audio, "Invalid input revision, revision=0x{:08X}", behavior_in.revision);
-        return false;
-    }
-
-    // Make sure that our behavior info revision matches the input
-    if (in_behavior_info.GetUserRevision() != behavior_in.revision) {
-        LOG_ERROR(Audio,
-                  "User revision differs from input revision, expecting 0x{:08X} but got 0x{:08X}",
-                  in_behavior_info.GetUserRevision(), behavior_in.revision);
-        return false;
-    }
-
-    // Update behavior info flags
-    in_behavior_info.ClearError();
-    in_behavior_info.UpdateFlags(behavior_in.flags);
-
-    return true;
-}
-
-bool InfoUpdater::UpdateMemoryPools(std::vector<ServerMemoryPoolInfo>& memory_pool_info) {
-    const auto memory_pool_count = memory_pool_info.size();
-    const auto total_memory_pool_in = sizeof(ServerMemoryPoolInfo::InParams) * memory_pool_count;
-    const auto total_memory_pool_out = sizeof(ServerMemoryPoolInfo::OutParams) * memory_pool_count;
-
-    if (input_header.size.memory_pool != total_memory_pool_in) {
-        LOG_ERROR(Audio, "Memory pools are an invalid size, expecting 0x{:X} but got 0x{:X}",
-                  total_memory_pool_in, input_header.size.memory_pool);
-        return false;
-    }
-
-    if (!AudioCommon::CanConsumeBuffer(in_params.size(), input_offset, total_memory_pool_in)) {
-        LOG_ERROR(Audio, "Buffer is an invalid size!");
-        return false;
-    }
-
-    std::vector<ServerMemoryPoolInfo::InParams> mempool_in(memory_pool_count);
-    std::vector<ServerMemoryPoolInfo::OutParams> mempool_out(memory_pool_count);
-
-    std::memcpy(mempool_in.data(), in_params.data() + input_offset, total_memory_pool_in);
-    input_offset += total_memory_pool_in;
-
-    // Update our memory pools
-    for (std::size_t i = 0; i < memory_pool_count; i++) {
-        if (!memory_pool_info[i].Update(mempool_in[i], mempool_out[i])) {
-            LOG_ERROR(Audio, "Failed to update memory pool {}!", i);
-            return false;
-        }
-    }
-
-    if (!AudioCommon::CanConsumeBuffer(out_params.size(), output_offset,
-                                       sizeof(BehaviorInfo::InParams))) {
-        LOG_ERROR(Audio, "Buffer is an invalid size!");
-        return false;
-    }
-
-    std::memcpy(out_params.data() + output_offset, mempool_out.data(), total_memory_pool_out);
-    output_offset += total_memory_pool_out;
-    output_header.size.memory_pool = static_cast<u32>(total_memory_pool_out);
-    return true;
-}
-
-bool InfoUpdater::UpdateVoiceChannelResources(VoiceContext& voice_context) {
-    const auto voice_count = voice_context.GetVoiceCount();
-    const auto voice_size = voice_count * sizeof(VoiceChannelResource::InParams);
-    std::vector<VoiceChannelResource::InParams> resources_in(voice_count);
-
-    if (input_header.size.voice_channel_resource != voice_size) {
-        LOG_ERROR(Audio, "VoiceChannelResource is an invalid size, expecting 0x{:X} but got 0x{:X}",
-                  voice_size, input_header.size.voice_channel_resource);
-        return false;
-    }
-
-    if (!AudioCommon::CanConsumeBuffer(in_params.size(), input_offset, voice_size)) {
-        LOG_ERROR(Audio, "Buffer is an invalid size!");
-        return false;
-    }
-
-    std::memcpy(resources_in.data(), in_params.data() + input_offset, voice_size);
-    input_offset += voice_size;
-
-    // Update our channel resources
-    for (std::size_t i = 0; i < voice_count; i++) {
-        // Grab our channel resource
-        auto& resource = voice_context.GetChannelResource(i);
-        resource.Update(resources_in[i]);
-    }
-
-    return true;
-}
-
-bool InfoUpdater::UpdateVoices(VoiceContext& voice_context,
-                               [[maybe_unused]] std::vector<ServerMemoryPoolInfo>& memory_pool_info,
-                               [[maybe_unused]] VAddr audio_codec_dsp_addr) {
-    const auto voice_count = voice_context.GetVoiceCount();
-    std::vector<VoiceInfo::InParams> voice_in(voice_count);
-    std::vector<VoiceInfo::OutParams> voice_out(voice_count);
-
-    const auto voice_in_size = voice_count * sizeof(VoiceInfo::InParams);
-    const auto voice_out_size = voice_count * sizeof(VoiceInfo::OutParams);
-
-    if (input_header.size.voice != voice_in_size) {
-        LOG_ERROR(Audio, "Voices are an invalid size, expecting 0x{:X} but got 0x{:X}",
-                  voice_in_size, input_header.size.voice);
-        return false;
-    }
-
-    if (!AudioCommon::CanConsumeBuffer(in_params.size(), input_offset, voice_in_size)) {
-        LOG_ERROR(Audio, "Buffer is an invalid size!");
-        return false;
-    }
-
-    std::memcpy(voice_in.data(), in_params.data() + input_offset, voice_in_size);
-    input_offset += voice_in_size;
-
-    // Set all voices to not be in use
-    for (std::size_t i = 0; i < voice_count; i++) {
-        voice_context.GetInfo(i).GetInParams().in_use = false;
-    }
-
-    // Update our voices
-    for (std::size_t i = 0; i < voice_count; i++) {
-        auto& voice_in_params = voice_in[i];
-        const auto channel_count = static_cast<std::size_t>(voice_in_params.channel_count);
-        // Skip if it's not currently in use
-        if (!voice_in_params.is_in_use) {
-            continue;
-        }
-        // Voice states for each channel
-        std::array<VoiceState*, AudioCommon::MAX_CHANNEL_COUNT> voice_states{};
-        ASSERT(static_cast<std::size_t>(voice_in_params.id) < voice_count);
-
-        // Grab our current voice info
-        auto& voice_info = voice_context.GetInfo(static_cast<std::size_t>(voice_in_params.id));
-
-        ASSERT(channel_count <= AudioCommon::MAX_CHANNEL_COUNT);
-
-        // Get all our channel voice states
-        for (std::size_t channel = 0; channel < channel_count; channel++) {
-            voice_states[channel] =
-                &voice_context.GetState(voice_in_params.voice_channel_resource_ids[channel]);
-        }
-
-        if (voice_in_params.is_new) {
-            // Default our values for our voice
-            voice_info.Initialize();
-
-            // Zero out our voice states
-            for (std::size_t channel = 0; channel < channel_count; channel++) {
-                std::memset(voice_states[channel], 0, sizeof(VoiceState));
-            }
-        }
-
-        // Update our voice
-        voice_info.UpdateParameters(voice_in_params, behavior_info);
-        // TODO(ogniK): Handle mapping errors with behavior info based on in params response
-
-        // Update our wave buffers
-        voice_info.UpdateWaveBuffers(voice_in_params, voice_states, behavior_info);
-        voice_info.WriteOutStatus(voice_out[i], voice_in_params, voice_states);
-    }
-
-    if (!AudioCommon::CanConsumeBuffer(out_params.size(), output_offset, voice_out_size)) {
-        LOG_ERROR(Audio, "Buffer is an invalid size!");
-        return false;
-    }
-    std::memcpy(out_params.data() + output_offset, voice_out.data(), voice_out_size);
-    output_offset += voice_out_size;
-    output_header.size.voice = static_cast<u32>(voice_out_size);
-    return true;
-}
-
-bool InfoUpdater::UpdateEffects(EffectContext& effect_context, bool is_active) {
-    const auto effect_count = effect_context.GetCount();
-    std::vector<EffectInfo::InParams> effect_in(effect_count);
-    std::vector<EffectInfo::OutParams> effect_out(effect_count);
-
-    const auto total_effect_in = effect_count * sizeof(EffectInfo::InParams);
-    const auto total_effect_out = effect_count * sizeof(EffectInfo::OutParams);
-
-    if (input_header.size.effect != total_effect_in) {
-        LOG_ERROR(Audio, "Effects are an invalid size, expecting 0x{:X} but got 0x{:X}",
-                  total_effect_in, input_header.size.effect);
-        return false;
-    }
-
-    if (!AudioCommon::CanConsumeBuffer(in_params.size(), input_offset, total_effect_in)) {
-        LOG_ERROR(Audio, "Buffer is an invalid size!");
-        return false;
-    }
-
-    std::memcpy(effect_in.data(), in_params.data() + input_offset, total_effect_in);
-    input_offset += total_effect_in;
-
-    // Update effects
-    for (std::size_t i = 0; i < effect_count; i++) {
-        auto* info = effect_context.GetInfo(i);
-        if (effect_in[i].type != info->GetType()) {
-            info = effect_context.RetargetEffect(i, effect_in[i].type);
-        }
-
-        info->Update(effect_in[i]);
-
-        if ((!is_active && info->GetUsage() != UsageState::Initialized) ||
-            info->GetUsage() == UsageState::Stopped) {
-            effect_out[i].status = UsageStatus::Removed;
-        } else {
-            effect_out[i].status = UsageStatus::Used;
-        }
-    }
-
-    if (!AudioCommon::CanConsumeBuffer(out_params.size(), output_offset, total_effect_out)) {
-        LOG_ERROR(Audio, "Buffer is an invalid size!");
-        return false;
-    }
-
-    std::memcpy(out_params.data() + output_offset, effect_out.data(), total_effect_out);
-    output_offset += total_effect_out;
-    output_header.size.effect = static_cast<u32>(total_effect_out);
-
-    return true;
-}
-
-bool InfoUpdater::UpdateSplitterInfo(SplitterContext& splitter_context) {
-    std::size_t start_offset = input_offset;
-    std::size_t bytes_read{};
-    // Update splitter context
-    if (!splitter_context.Update(in_params, input_offset, bytes_read)) {
-        LOG_ERROR(Audio, "Failed to update splitter context!");
-        return false;
-    }
-
-    const auto consumed = input_offset - start_offset;
-
-    if (input_header.size.splitter != consumed) {
-        LOG_ERROR(Audio, "Splitters is an invalid size, expecting 0x{:X} but got 0x{:X}",
-                  bytes_read, input_header.size.splitter);
-        return false;
-    }
-
-    return true;
-}
-
-Result InfoUpdater::UpdateMixes(MixContext& mix_context, std::size_t mix_buffer_count,
-                                SplitterContext& splitter_context, EffectContext& effect_context) {
-    std::vector<MixInfo::InParams> mix_in_params;
-
-    if (!behavior_info.IsMixInParameterDirtyOnlyUpdateSupported()) {
-        // If we're not dirty, get ALL mix in parameters
-        const auto context_mix_count = mix_context.GetCount();
-        const auto total_mix_in = context_mix_count * sizeof(MixInfo::InParams);
-        if (input_header.size.mixer != total_mix_in) {
-            LOG_ERROR(Audio, "Mixer is an invalid size, expecting 0x{:X} but got 0x{:X}",
-                      total_mix_in, input_header.size.mixer);
-            return AudioCommon::Audren::ERR_INVALID_PARAMETERS;
-        }
-
-        if (!AudioCommon::CanConsumeBuffer(in_params.size(), input_offset, total_mix_in)) {
-            LOG_ERROR(Audio, "Buffer is an invalid size!");
-            return AudioCommon::Audren::ERR_INVALID_PARAMETERS;
-        }
-
-        mix_in_params.resize(context_mix_count);
-        std::memcpy(mix_in_params.data(), in_params.data() + input_offset, total_mix_in);
-
-        input_offset += total_mix_in;
-    } else {
-        // Only update the "dirty" mixes
-        MixInfo::DirtyHeader dirty_header{};
-        if (!AudioCommon::CanConsumeBuffer(in_params.size(), input_offset,
-                                           sizeof(MixInfo::DirtyHeader))) {
-            LOG_ERROR(Audio, "Buffer is an invalid size!");
-            return AudioCommon::Audren::ERR_INVALID_PARAMETERS;
-        }
-
-        std::memcpy(&dirty_header, in_params.data() + input_offset, sizeof(MixInfo::DirtyHeader));
-        input_offset += sizeof(MixInfo::DirtyHeader);
-
-        const auto total_mix_in =
-            dirty_header.mixer_count * sizeof(MixInfo::InParams) + sizeof(MixInfo::DirtyHeader);
-
-        if (input_header.size.mixer != total_mix_in) {
-            LOG_ERROR(Audio, "Mixer is an invalid size, expecting 0x{:X} but got 0x{:X}",
-                      total_mix_in, input_header.size.mixer);
-            return AudioCommon::Audren::ERR_INVALID_PARAMETERS;
-        }
-
-        if (dirty_header.mixer_count != 0) {
-            mix_in_params.resize(dirty_header.mixer_count);
-            std::memcpy(mix_in_params.data(), in_params.data() + input_offset,
-                        mix_in_params.size() * sizeof(MixInfo::InParams));
-            input_offset += mix_in_params.size() * sizeof(MixInfo::InParams);
-        }
-    }
-
-    // Get our total input count
-    const auto mix_count = mix_in_params.size();
-
-    if (!behavior_info.IsMixInParameterDirtyOnlyUpdateSupported()) {
-        // Only verify our buffer count if we're not dirty
-        std::size_t total_buffer_count{};
-        for (std::size_t i = 0; i < mix_count; i++) {
-            const auto& in = mix_in_params[i];
-            total_buffer_count += in.buffer_count;
-            if (static_cast<std::size_t>(in.dest_mix_id) > mix_count &&
-                in.dest_mix_id != AudioCommon::NO_MIX && in.mix_id != AudioCommon::FINAL_MIX) {
-                LOG_ERROR(
-                    Audio,
-                    "Invalid mix destination, mix_id={:X}, dest_mix_id={:X}, mix_buffer_count={:X}",
-                    in.mix_id, in.dest_mix_id, mix_buffer_count);
-                return AudioCommon::Audren::ERR_INVALID_PARAMETERS;
-            }
-        }
-
-        if (total_buffer_count > mix_buffer_count) {
-            LOG_ERROR(Audio,
-                      "Too many mix buffers used! mix_buffer_count={:X}, requesting_buffers={:X}",
-                      mix_buffer_count, total_buffer_count);
-            return AudioCommon::Audren::ERR_INVALID_PARAMETERS;
-        }
-    }
-
-    if (mix_buffer_count == 0) {
-        LOG_ERROR(Audio, "No mix buffers!");
-        return AudioCommon::Audren::ERR_INVALID_PARAMETERS;
-    }
-
-    bool should_sort = false;
-    for (std::size_t i = 0; i < mix_count; i++) {
-        const auto& mix_in = mix_in_params[i];
-        std::size_t target_mix{};
-        if (behavior_info.IsMixInParameterDirtyOnlyUpdateSupported()) {
-            target_mix = mix_in.mix_id;
-        } else {
-            // Non dirty supported games just use i instead of the actual mix_id
-            target_mix = i;
-        }
-        auto& mix_info = mix_context.GetInfo(target_mix);
-        auto& mix_info_params = mix_info.GetInParams();
-        if (mix_info_params.in_use != mix_in.in_use) {
-            mix_info_params.in_use = mix_in.in_use;
-            mix_info.ResetEffectProcessingOrder();
-            should_sort = true;
-        }
-
-        if (mix_in.in_use) {
-            should_sort |= mix_info.Update(mix_context.GetEdgeMatrix(), mix_in, behavior_info,
-                                           splitter_context, effect_context);
-        }
-    }
-
-    if (should_sort && behavior_info.IsSplitterSupported()) {
-        // Sort our splitter data
-        if (!mix_context.TsortInfo(splitter_context)) {
-            return AudioCommon::Audren::ERR_SPLITTER_SORT_FAILED;
-        }
-    }
-
-    // TODO(ogniK): Sort when splitter is suppoorted
-
-    return ResultSuccess;
-}
-
-bool InfoUpdater::UpdateSinks(SinkContext& sink_context) {
-    const auto sink_count = sink_context.GetCount();
-    std::vector<SinkInfo::InParams> sink_in_params(sink_count);
-    const auto total_sink_in = sink_count * sizeof(SinkInfo::InParams);
-
-    if (input_header.size.sink != total_sink_in) {
-        LOG_ERROR(Audio, "Sinks are an invalid size, expecting 0x{:X} but got 0x{:X}",
-                  total_sink_in, input_header.size.effect);
-        return false;
-    }
-
-    if (!AudioCommon::CanConsumeBuffer(in_params.size(), input_offset, total_sink_in)) {
-        LOG_ERROR(Audio, "Buffer is an invalid size!");
-        return false;
-    }
-
-    std::memcpy(sink_in_params.data(), in_params.data() + input_offset, total_sink_in);
-    input_offset += total_sink_in;
-
-    // TODO(ogniK): Properly update sinks
-    if (!sink_in_params.empty()) {
-        sink_context.UpdateMainSink(sink_in_params[0]);
-    }
-
-    output_header.size.sink = static_cast<u32>(0x20 * sink_count);
-    output_offset += 0x20 * sink_count;
-    return true;
-}
-
-bool InfoUpdater::UpdatePerformanceBuffer() {
-    output_header.size.performance = 0x10;
-    output_offset += 0x10;
-    return true;
-}
-
-bool InfoUpdater::UpdateErrorInfo([[maybe_unused]] BehaviorInfo& in_behavior_info) {
-    const auto total_beahvior_info_out = sizeof(BehaviorInfo::OutParams);
-
-    if (!AudioCommon::CanConsumeBuffer(out_params.size(), output_offset, total_beahvior_info_out)) {
-        LOG_ERROR(Audio, "Buffer is an invalid size!");
-        return false;
-    }
-
-    BehaviorInfo::OutParams behavior_info_out{};
-    behavior_info.CopyErrorInfo(behavior_info_out);
-
-    std::memcpy(out_params.data() + output_offset, &behavior_info_out, total_beahvior_info_out);
-    output_offset += total_beahvior_info_out;
-    output_header.size.behavior = total_beahvior_info_out;
-
-    return true;
-}
-
-struct RendererInfo {
-    u64_le elasped_frame_count{};
-    INSERT_PADDING_WORDS(2);
-};
-static_assert(sizeof(RendererInfo) == 0x10, "RendererInfo is an invalid size");
-
-bool InfoUpdater::UpdateRendererInfo(std::size_t elapsed_frame_count) {
-    const auto total_renderer_info_out = sizeof(RendererInfo);
-    if (!AudioCommon::CanConsumeBuffer(out_params.size(), output_offset, total_renderer_info_out)) {
-        LOG_ERROR(Audio, "Buffer is an invalid size!");
-        return false;
-    }
-    RendererInfo out{};
-    out.elasped_frame_count = elapsed_frame_count;
-    std::memcpy(out_params.data() + output_offset, &out, total_renderer_info_out);
-    output_offset += total_renderer_info_out;
-    output_header.size.render_info = total_renderer_info_out;
-
-    return true;
-}
-
-bool InfoUpdater::CheckConsumedSize() const {
-    if (output_offset != out_params.size()) {
-        LOG_ERROR(Audio, "Output is not consumed! Consumed {}, but requires {}. {} bytes remaining",
-                  output_offset, out_params.size(), out_params.size() - output_offset);
-        return false;
-    }
-    /*if (input_offset != in_params.size()) {
-        LOG_ERROR(Audio, "Input is not consumed!");
-        return false;
-    }*/
-    return true;
-}
-
-bool InfoUpdater::WriteOutputHeader() {
-    if (!AudioCommon::CanConsumeBuffer(out_params.size(), 0,
-                                       sizeof(AudioCommon::UpdateDataHeader))) {
-        LOG_ERROR(Audio, "Buffer is an invalid size!");
-        return false;
-    }
-    output_header.revision = AudioCommon::CURRENT_PROCESS_REVISION;
-    const auto& sz = output_header.size;
-    output_header.total_size += sz.behavior + sz.memory_pool + sz.voice +
-                                sz.voice_channel_resource + sz.effect + sz.mixer + sz.sink +
-                                sz.performance + sz.splitter + sz.render_info;
-
-    std::memcpy(out_params.data(), &output_header, sizeof(AudioCommon::UpdateDataHeader));
-    return true;
-}
-
-} // namespace AudioCore
diff --git a/src/audio_core/info_updater.h b/src/audio_core/info_updater.h
deleted file mode 100644
index 17e66b0363..0000000000
--- a/src/audio_core/info_updater.h
+++ /dev/null
@@ -1,57 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include <vector>
-#include "audio_core/common.h"
-#include "common/common_types.h"
-
-namespace AudioCore {
-
-class BehaviorInfo;
-class ServerMemoryPoolInfo;
-class VoiceContext;
-class EffectContext;
-class MixContext;
-class SinkContext;
-class SplitterContext;
-
-class InfoUpdater {
-public:
-    // TODO(ogniK): Pass process handle when we support it
-    InfoUpdater(const std::vector<u8>& in_params_, std::vector<u8>& out_params_,
-                BehaviorInfo& behavior_info_);
-    ~InfoUpdater();
-
-    bool UpdateBehaviorInfo(BehaviorInfo& in_behavior_info);
-    bool UpdateMemoryPools(std::vector<ServerMemoryPoolInfo>& memory_pool_info);
-    bool UpdateVoiceChannelResources(VoiceContext& voice_context);
-    bool UpdateVoices(VoiceContext& voice_context,
-                      std::vector<ServerMemoryPoolInfo>& memory_pool_info,
-                      VAddr audio_codec_dsp_addr);
-    bool UpdateEffects(EffectContext& effect_context, bool is_active);
-    bool UpdateSplitterInfo(SplitterContext& splitter_context);
-    Result UpdateMixes(MixContext& mix_context, std::size_t mix_buffer_count,
-                       SplitterContext& splitter_context, EffectContext& effect_context);
-    bool UpdateSinks(SinkContext& sink_context);
-    bool UpdatePerformanceBuffer();
-    bool UpdateErrorInfo(BehaviorInfo& in_behavior_info);
-    bool UpdateRendererInfo(std::size_t elapsed_frame_count);
-    bool CheckConsumedSize() const;
-
-    bool WriteOutputHeader();
-
-private:
-    const std::vector<u8>& in_params;
-    std::vector<u8>& out_params;
-    BehaviorInfo& behavior_info;
-
-    AudioCommon::UpdateDataHeader input_header{};
-    AudioCommon::UpdateDataHeader output_header{};
-
-    std::size_t input_offset{sizeof(AudioCommon::UpdateDataHeader)};
-    std::size_t output_offset{sizeof(AudioCommon::UpdateDataHeader)};
-};
-
-} // namespace AudioCore
diff --git a/src/audio_core/memory_pool.cpp b/src/audio_core/memory_pool.cpp
deleted file mode 100644
index 627e5f15e6..0000000000
--- a/src/audio_core/memory_pool.cpp
+++ /dev/null
@@ -1,60 +0,0 @@
-
-// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include "audio_core/memory_pool.h"
-#include "common/logging/log.h"
-
-namespace AudioCore {
-
-ServerMemoryPoolInfo::ServerMemoryPoolInfo() = default;
-ServerMemoryPoolInfo::~ServerMemoryPoolInfo() = default;
-
-bool ServerMemoryPoolInfo::Update(const InParams& in_params, OutParams& out_params) {
-    // Our state does not need to be changed
-    if (in_params.state != State::RequestAttach && in_params.state != State::RequestDetach) {
-        return true;
-    }
-
-    // Address or size is null
-    if (in_params.address == 0 || in_params.size == 0) {
-        LOG_ERROR(Audio, "Memory pool address or size is zero! address={:X}, size={:X}",
-                  in_params.address, in_params.size);
-        return false;
-    }
-
-    // Address or size is not aligned
-    if ((in_params.address % 0x1000) != 0 || (in_params.size % 0x1000) != 0) {
-        LOG_ERROR(Audio, "Memory pool address or size is not aligned! address={:X}, size={:X}",
-                  in_params.address, in_params.size);
-        return false;
-    }
-
-    if (in_params.state == State::RequestAttach) {
-        cpu_address = in_params.address;
-        size = in_params.size;
-        used = true;
-        out_params.state = State::Attached;
-    } else {
-        // Unexpected address
-        if (cpu_address != in_params.address) {
-            LOG_ERROR(Audio, "Memory pool address differs! Expecting {:X} but address is {:X}",
-                      cpu_address, in_params.address);
-            return false;
-        }
-
-        if (size != in_params.size) {
-            LOG_ERROR(Audio, "Memory pool size differs! Expecting {:X} but size is {:X}", size,
-                      in_params.size);
-            return false;
-        }
-
-        cpu_address = 0;
-        size = 0;
-        used = false;
-        out_params.state = State::Detached;
-    }
-    return true;
-}
-
-} // namespace AudioCore
diff --git a/src/audio_core/memory_pool.h b/src/audio_core/memory_pool.h
deleted file mode 100644
index e71bc025bc..0000000000
--- a/src/audio_core/memory_pool.h
+++ /dev/null
@@ -1,51 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include "common/common_funcs.h"
-#include "common/common_types.h"
-#include "common/swap.h"
-
-namespace AudioCore {
-
-class ServerMemoryPoolInfo {
-public:
-    ServerMemoryPoolInfo();
-    ~ServerMemoryPoolInfo();
-
-    enum class State : u32_le {
-        Invalid = 0x0,
-        Aquired = 0x1,
-        RequestDetach = 0x2,
-        Detached = 0x3,
-        RequestAttach = 0x4,
-        Attached = 0x5,
-        Released = 0x6,
-    };
-
-    struct InParams {
-        u64_le address{};
-        u64_le size{};
-        State state{};
-        INSERT_PADDING_WORDS(3);
-    };
-    static_assert(sizeof(InParams) == 0x20, "InParams are an invalid size");
-
-    struct OutParams {
-        State state{};
-        INSERT_PADDING_WORDS(3);
-    };
-    static_assert(sizeof(OutParams) == 0x10, "OutParams are an invalid size");
-
-    bool Update(const InParams& in_params, OutParams& out_params);
-
-private:
-    // There's another entry here which is the DSP address, however since we're not talking to the
-    // DSP we can just use the same address provided by the guest without needing to remap
-    u64_le cpu_address{};
-    u64_le size{};
-    bool used{};
-};
-
-} // namespace AudioCore
diff --git a/src/audio_core/mix_context.cpp b/src/audio_core/mix_context.cpp
deleted file mode 100644
index bcaa7afabd..0000000000
--- a/src/audio_core/mix_context.cpp
+++ /dev/null
@@ -1,297 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include <algorithm>
-
-#include "audio_core/behavior_info.h"
-#include "audio_core/common.h"
-#include "audio_core/effect_context.h"
-#include "audio_core/mix_context.h"
-#include "audio_core/splitter_context.h"
-
-namespace AudioCore {
-MixContext::MixContext() = default;
-MixContext::~MixContext() = default;
-
-void MixContext::Initialize(const BehaviorInfo& behavior_info, std::size_t mix_count,
-                            std::size_t effect_count) {
-    info_count = mix_count;
-    infos.resize(info_count);
-    auto& final_mix = GetInfo(AudioCommon::FINAL_MIX);
-    final_mix.GetInParams().mix_id = AudioCommon::FINAL_MIX;
-    sorted_info.reserve(infos.size());
-    for (auto& info : infos) {
-        sorted_info.push_back(&info);
-    }
-
-    for (auto& info : infos) {
-        info.SetEffectCount(effect_count);
-    }
-
-    // Only initialize our edge matrix and node states if splitters are supported
-    if (behavior_info.IsSplitterSupported()) {
-        node_states.Initialize(mix_count);
-        edge_matrix.Initialize(mix_count);
-    }
-}
-
-void MixContext::UpdateDistancesFromFinalMix() {
-    // Set all distances to be invalid
-    for (std::size_t i = 0; i < info_count; i++) {
-        GetInfo(i).GetInParams().final_mix_distance = AudioCommon::NO_FINAL_MIX;
-    }
-
-    for (std::size_t i = 0; i < info_count; i++) {
-        auto& info = GetInfo(i);
-        auto& in_params = info.GetInParams();
-        // Populate our sorted info
-        sorted_info[i] = &info;
-
-        if (!in_params.in_use) {
-            continue;
-        }
-
-        auto mix_id = in_params.mix_id;
-        // Needs to be referenced out of scope
-        s32 distance_to_final_mix{AudioCommon::FINAL_MIX};
-        for (; distance_to_final_mix < static_cast<s32>(info_count); distance_to_final_mix++) {
-            if (mix_id == AudioCommon::FINAL_MIX) {
-                // If we're at the final mix, we're done
-                break;
-            } else if (mix_id == AudioCommon::NO_MIX) {
-                // If we have no more mix ids, we're done
-                distance_to_final_mix = AudioCommon::NO_FINAL_MIX;
-                break;
-            } else {
-                const auto& dest_mix = GetInfo(mix_id);
-                const auto dest_mix_distance = dest_mix.GetInParams().final_mix_distance;
-
-                if (dest_mix_distance == AudioCommon::NO_FINAL_MIX) {
-                    // If our current mix isn't pointing to a final mix, follow through
-                    mix_id = dest_mix.GetInParams().dest_mix_id;
-                } else {
-                    // Our current mix + 1 = final distance
-                    distance_to_final_mix = dest_mix_distance + 1;
-                    break;
-                }
-            }
-        }
-
-        // If we're out of range for our distance, mark it as no final mix
-        if (distance_to_final_mix >= static_cast<s32>(info_count)) {
-            distance_to_final_mix = AudioCommon::NO_FINAL_MIX;
-        }
-
-        in_params.final_mix_distance = distance_to_final_mix;
-    }
-}
-
-void MixContext::CalcMixBufferOffset() {
-    s32 offset{};
-    for (std::size_t i = 0; i < info_count; i++) {
-        auto& info = GetSortedInfo(i);
-        auto& in_params = info.GetInParams();
-        if (in_params.in_use) {
-            // Only update if in use
-            in_params.buffer_offset = offset;
-            offset += in_params.buffer_count;
-        }
-    }
-}
-
-void MixContext::SortInfo() {
-    // Get the distance to the final mix
-    UpdateDistancesFromFinalMix();
-
-    // Sort based on the distance to the final mix
-    std::sort(sorted_info.begin(), sorted_info.end(),
-              [](const ServerMixInfo* lhs, const ServerMixInfo* rhs) {
-                  return lhs->GetInParams().final_mix_distance >
-                         rhs->GetInParams().final_mix_distance;
-              });
-
-    // Calculate the mix buffer offset
-    CalcMixBufferOffset();
-}
-
-bool MixContext::TsortInfo(SplitterContext& splitter_context) {
-    // If we're not using mixes, just calculate the mix buffer offset
-    if (!splitter_context.UsingSplitter()) {
-        CalcMixBufferOffset();
-        return true;
-    }
-    // Sort our node states
-    if (!node_states.Tsort(edge_matrix)) {
-        return false;
-    }
-
-    // Get our sorted list
-    const auto sorted_list = node_states.GetIndexList();
-    std::size_t info_id{};
-    for (auto itr = sorted_list.rbegin(); itr != sorted_list.rend(); ++itr) {
-        // Set our sorted info
-        sorted_info[info_id++] = &GetInfo(*itr);
-    }
-
-    // Calculate the mix buffer offset
-    CalcMixBufferOffset();
-    return true;
-}
-
-std::size_t MixContext::GetCount() const {
-    return info_count;
-}
-
-ServerMixInfo& MixContext::GetInfo(std::size_t i) {
-    ASSERT(i < info_count);
-    return infos.at(i);
-}
-
-const ServerMixInfo& MixContext::GetInfo(std::size_t i) const {
-    ASSERT(i < info_count);
-    return infos.at(i);
-}
-
-ServerMixInfo& MixContext::GetSortedInfo(std::size_t i) {
-    ASSERT(i < info_count);
-    return *sorted_info.at(i);
-}
-
-const ServerMixInfo& MixContext::GetSortedInfo(std::size_t i) const {
-    ASSERT(i < info_count);
-    return *sorted_info.at(i);
-}
-
-ServerMixInfo& MixContext::GetFinalMixInfo() {
-    return infos.at(AudioCommon::FINAL_MIX);
-}
-
-const ServerMixInfo& MixContext::GetFinalMixInfo() const {
-    return infos.at(AudioCommon::FINAL_MIX);
-}
-
-EdgeMatrix& MixContext::GetEdgeMatrix() {
-    return edge_matrix;
-}
-
-const EdgeMatrix& MixContext::GetEdgeMatrix() const {
-    return edge_matrix;
-}
-
-ServerMixInfo::ServerMixInfo() {
-    Cleanup();
-}
-ServerMixInfo::~ServerMixInfo() = default;
-
-const ServerMixInfo::InParams& ServerMixInfo::GetInParams() const {
-    return in_params;
-}
-
-ServerMixInfo::InParams& ServerMixInfo::GetInParams() {
-    return in_params;
-}
-
-bool ServerMixInfo::Update(EdgeMatrix& edge_matrix, const MixInfo::InParams& mix_in,
-                           BehaviorInfo& behavior_info, SplitterContext& splitter_context,
-                           EffectContext& effect_context) {
-    in_params.volume = mix_in.volume;
-    in_params.sample_rate = mix_in.sample_rate;
-    in_params.buffer_count = mix_in.buffer_count;
-    in_params.in_use = mix_in.in_use;
-    in_params.mix_id = mix_in.mix_id;
-    in_params.node_id = mix_in.node_id;
-    for (std::size_t i = 0; i < mix_in.mix_volume.size(); i++) {
-        std::copy(mix_in.mix_volume[i].begin(), mix_in.mix_volume[i].end(),
-                  in_params.mix_volume[i].begin());
-    }
-
-    bool require_sort = false;
-
-    if (behavior_info.IsSplitterSupported()) {
-        require_sort = UpdateConnection(edge_matrix, mix_in, splitter_context);
-    } else {
-        in_params.dest_mix_id = mix_in.dest_mix_id;
-        in_params.splitter_id = AudioCommon::NO_SPLITTER;
-    }
-
-    ResetEffectProcessingOrder();
-    const auto effect_count = effect_context.GetCount();
-    for (std::size_t i = 0; i < effect_count; i++) {
-        auto* effect_info = effect_context.GetInfo(i);
-        if (effect_info->GetMixID() == in_params.mix_id) {
-            effect_processing_order[effect_info->GetProcessingOrder()] = static_cast<s32>(i);
-        }
-    }
-
-    // TODO(ogniK): Update effect processing order
-    return require_sort;
-}
-
-bool ServerMixInfo::HasAnyConnection() const {
-    return in_params.splitter_id != AudioCommon::NO_SPLITTER ||
-           in_params.mix_id != AudioCommon::NO_MIX;
-}
-
-void ServerMixInfo::Cleanup() {
-    in_params.volume = 0.0f;
-    in_params.sample_rate = 0;
-    in_params.buffer_count = 0;
-    in_params.in_use = false;
-    in_params.mix_id = AudioCommon::NO_MIX;
-    in_params.node_id = 0;
-    in_params.buffer_offset = 0;
-    in_params.dest_mix_id = AudioCommon::NO_MIX;
-    in_params.splitter_id = AudioCommon::NO_SPLITTER;
-    std::memset(in_params.mix_volume.data(), 0, sizeof(float) * in_params.mix_volume.size());
-}
-
-void ServerMixInfo::SetEffectCount(std::size_t count) {
-    effect_processing_order.resize(count);
-    ResetEffectProcessingOrder();
-}
-
-void ServerMixInfo::ResetEffectProcessingOrder() {
-    for (auto& order : effect_processing_order) {
-        order = AudioCommon::NO_EFFECT_ORDER;
-    }
-}
-
-s32 ServerMixInfo::GetEffectOrder(std::size_t i) const {
-    return effect_processing_order.at(i);
-}
-
-bool ServerMixInfo::UpdateConnection(EdgeMatrix& edge_matrix, const MixInfo::InParams& mix_in,
-                                     SplitterContext& splitter_context) {
-    // Mixes are identical
-    if (in_params.dest_mix_id == mix_in.dest_mix_id &&
-        in_params.splitter_id == mix_in.splitter_id &&
-        ((in_params.splitter_id == AudioCommon::NO_SPLITTER) ||
-         !splitter_context.GetInfo(in_params.splitter_id).HasNewConnection())) {
-        return false;
-    }
-    // Remove current edges for mix id
-    edge_matrix.RemoveEdges(in_params.mix_id);
-    if (mix_in.dest_mix_id != AudioCommon::NO_MIX) {
-        // If we have a valid destination mix id, set our edge matrix
-        edge_matrix.Connect(in_params.mix_id, mix_in.dest_mix_id);
-    } else if (mix_in.splitter_id != AudioCommon::NO_SPLITTER) {
-        // Recurse our splitter linked and set our edges
-        auto& splitter_info = splitter_context.GetInfo(mix_in.splitter_id);
-        const auto length = splitter_info.GetLength();
-        for (s32 i = 0; i < length; i++) {
-            const auto* splitter_destination =
-                splitter_context.GetDestinationData(mix_in.splitter_id, i);
-            if (splitter_destination == nullptr) {
-                continue;
-            }
-            if (splitter_destination->ValidMixId()) {
-                edge_matrix.Connect(in_params.mix_id, splitter_destination->GetMixId());
-            }
-        }
-    }
-    in_params.dest_mix_id = mix_in.dest_mix_id;
-    in_params.splitter_id = mix_in.splitter_id;
-    return true;
-}
-
-} // namespace AudioCore
diff --git a/src/audio_core/mix_context.h b/src/audio_core/mix_context.h
deleted file mode 100644
index 3939c77e93..0000000000
--- a/src/audio_core/mix_context.h
+++ /dev/null
@@ -1,113 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include <array>
-#include <vector>
-#include "audio_core/common.h"
-#include "audio_core/splitter_context.h"
-#include "common/common_funcs.h"
-#include "common/common_types.h"
-
-namespace AudioCore {
-class BehaviorInfo;
-class EffectContext;
-
-class MixInfo {
-public:
-    struct DirtyHeader {
-        u32_le magic{};
-        u32_le mixer_count{};
-        INSERT_PADDING_BYTES(0x18);
-    };
-    static_assert(sizeof(DirtyHeader) == 0x20, "MixInfo::DirtyHeader is an invalid size");
-
-    struct InParams {
-        float_le volume{};
-        s32_le sample_rate{};
-        s32_le buffer_count{};
-        bool in_use{};
-        INSERT_PADDING_BYTES(3);
-        s32_le mix_id{};
-        s32_le effect_count{};
-        u32_le node_id{};
-        INSERT_PADDING_WORDS(2);
-        std::array<std::array<float_le, AudioCommon::MAX_MIX_BUFFERS>, AudioCommon::MAX_MIX_BUFFERS>
-            mix_volume{};
-        s32_le dest_mix_id{};
-        s32_le splitter_id{};
-        INSERT_PADDING_WORDS(1);
-    };
-    static_assert(sizeof(MixInfo::InParams) == 0x930, "MixInfo::InParams is an invalid size");
-};
-
-class ServerMixInfo {
-public:
-    struct InParams {
-        float volume{};
-        s32 sample_rate{};
-        s32 buffer_count{};
-        bool in_use{};
-        s32 mix_id{};
-        u32 node_id{};
-        std::array<std::array<float_le, AudioCommon::MAX_MIX_BUFFERS>, AudioCommon::MAX_MIX_BUFFERS>
-            mix_volume{};
-        s32 dest_mix_id{};
-        s32 splitter_id{};
-        s32 buffer_offset{};
-        s32 final_mix_distance{};
-    };
-    ServerMixInfo();
-    ~ServerMixInfo();
-
-    [[nodiscard]] const ServerMixInfo::InParams& GetInParams() const;
-    [[nodiscard]] ServerMixInfo::InParams& GetInParams();
-
-    bool Update(EdgeMatrix& edge_matrix, const MixInfo::InParams& mix_in,
-                BehaviorInfo& behavior_info, SplitterContext& splitter_context,
-                EffectContext& effect_context);
-    [[nodiscard]] bool HasAnyConnection() const;
-    void Cleanup();
-    void SetEffectCount(std::size_t count);
-    void ResetEffectProcessingOrder();
-    [[nodiscard]] s32 GetEffectOrder(std::size_t i) const;
-
-private:
-    std::vector<s32> effect_processing_order;
-    InParams in_params{};
-    bool UpdateConnection(EdgeMatrix& edge_matrix, const MixInfo::InParams& mix_in,
-                          SplitterContext& splitter_context);
-};
-
-class MixContext {
-public:
-    MixContext();
-    ~MixContext();
-
-    void Initialize(const BehaviorInfo& behavior_info, std::size_t mix_count,
-                    std::size_t effect_count);
-    void SortInfo();
-    bool TsortInfo(SplitterContext& splitter_context);
-
-    [[nodiscard]] std::size_t GetCount() const;
-    [[nodiscard]] ServerMixInfo& GetInfo(std::size_t i);
-    [[nodiscard]] const ServerMixInfo& GetInfo(std::size_t i) const;
-    [[nodiscard]] ServerMixInfo& GetSortedInfo(std::size_t i);
-    [[nodiscard]] const ServerMixInfo& GetSortedInfo(std::size_t i) const;
-    [[nodiscard]] ServerMixInfo& GetFinalMixInfo();
-    [[nodiscard]] const ServerMixInfo& GetFinalMixInfo() const;
-    [[nodiscard]] EdgeMatrix& GetEdgeMatrix();
-    [[nodiscard]] const EdgeMatrix& GetEdgeMatrix() const;
-
-private:
-    void CalcMixBufferOffset();
-    void UpdateDistancesFromFinalMix();
-
-    NodeStates node_states{};
-    EdgeMatrix edge_matrix{};
-    std::size_t info_count{};
-    std::vector<ServerMixInfo> infos{};
-    std::vector<ServerMixInfo*> sorted_info{};
-};
-} // namespace AudioCore
diff --git a/src/audio_core/null_sink.h b/src/audio_core/null_sink.h
deleted file mode 100644
index 37b2f7eff2..0000000000
--- a/src/audio_core/null_sink.h
+++ /dev/null
@@ -1,32 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include "audio_core/sink.h"
-
-namespace AudioCore {
-
-class NullSink final : public Sink {
-public:
-    explicit NullSink(std::string_view) {}
-    ~NullSink() override = default;
-
-    SinkStream& AcquireSinkStream(u32 /*sample_rate*/, u32 /*num_channels*/,
-                                  const std::string& /*name*/) override {
-        return null_sink_stream;
-    }
-
-private:
-    struct NullSinkStreamImpl final : SinkStream {
-        void EnqueueSamples(u32 /*num_channels*/, const std::vector<s16>& /*samples*/) override {}
-
-        std::size_t SamplesInQueue(u32 /*num_channels*/) const override {
-            return 0;
-        }
-
-        void Flush() override {}
-    } null_sink_stream;
-};
-
-} // namespace AudioCore
diff --git a/src/audio_core/out/audio_out.cpp b/src/audio_core/out/audio_out.cpp
new file mode 100644
index 0000000000..9a8d8a7421
--- /dev/null
+++ b/src/audio_core/out/audio_out.cpp
@@ -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::ERR_BUFFER_COUNT_EXCEEDED;
+}
+
+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() {
+    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) {
+    std::scoped_lock l{parent_mutex};
+    return system.ContainsAudioBuffer(tag);
+}
+
+u32 Out::GetBufferCount() {
+    std::scoped_lock l{parent_mutex};
+    return system.GetBufferCount();
+}
+
+u64 Out::GetPlayedSampleCount() {
+    std::scoped_lock l{parent_mutex};
+    return system.GetPlayedSampleCount();
+}
+
+} // namespace AudioCore::AudioOut
diff --git a/src/audio_core/out/audio_out.h b/src/audio_core/out/audio_out.h
new file mode 100644
index 0000000000..f6b9216451
--- /dev/null
+++ b/src/audio_core/out/audio_out.h
@@ -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();
+
+    /**
+     * 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);
+
+    /**
+     * Get the maximum number of buffers.
+     *
+     * @return The maximum number of buffers.
+     */
+    u32 GetBufferCount();
+
+    /**
+     * Get the total played sample count for this audio out.
+     *
+     * @return The played sample count.
+     */
+    u64 GetPlayedSampleCount();
+
+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
diff --git a/src/audio_core/out/audio_out_system.cpp b/src/audio_core/out/audio_out_system.cpp
new file mode 100644
index 0000000000..35afddf06d
--- /dev/null
+++ b/src/audio_core/out/audio_out_system.cpp
@@ -0,0 +1,207 @@
+// 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();
+    buffer_event->GetWritableEvent().Signal();
+}
+
+std::string_view System::GetDefaultOutputDeviceName() {
+    return "DeviceOut";
+}
+
+Result System::IsConfigValid(std::string_view device_name, const AudioOutParameter& in_params) {
+    if ((device_name.size() > 0) && (device_name != GetDefaultOutputDeviceName())) {
+        return Service::Audio::ERR_INVALID_DEVICE_NAME;
+    }
+
+    if (in_params.sample_rate != TargetSampleRate && in_params.sample_rate > 0) {
+        return Service::Audio::ERR_INVALID_SAMPLE_RATE;
+    }
+
+    if (in_params.channel_count == 0 || in_params.channel_count == 2 ||
+        in_params.channel_count == 6) {
+        return ResultSuccess;
+    }
+
+    return Service::Audio::ERR_INVALID_CHANNEL_COUNT;
+}
+
+Result System::Initialize(std::string& device_name, const AudioOutParameter& in_params, u32 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::ERR_OPERATION_FAILED;
+    }
+
+    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;
+
+    std::vector<AudioBuffer> buffers_to_flush{};
+    buffers.RegisterBuffers(buffers_to_flush);
+    session->AppendBuffers(buffers_to_flush);
+
+    return ResultSuccess;
+}
+
+Result System::Stop() {
+    if (state == State::Started) {
+        session->Stop();
+        session->SetVolume(0.0f);
+        state = State::Stopped;
+    }
+
+    return ResultSuccess;
+}
+
+bool System::AppendBuffer(const AudioOutBuffer& buffer, u64 tag) {
+    if (buffers.GetTotalBufferCount() == BufferCount) {
+        return false;
+    }
+
+    AudioBuffer new_buffer{
+        .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) {
+        std::vector<AudioBuffer> registered_buffers{};
+        buffers.RegisterBuffers(registered_buffers);
+        session->AppendBuffers(registered_buffers);
+    }
+}
+
+void System::ReleaseBuffers() {
+    bool signal{buffers.ReleaseBuffers(system.CoreTiming(), *session)};
+    if (signal) {
+        // Signal if any buffer was released, or if none are registered, we need more.
+        buffer_event->GetWritableEvent().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->GetWritableEvent().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) {
+    return buffers.ContainsBuffer(tag);
+}
+
+u32 System::GetBufferCount() {
+    return buffers.GetAppendedRegisteredCount();
+}
+
+u64 System::GetPlayedSampleCount() const {
+    return session->GetPlayedSampleCount();
+}
+
+} // namespace AudioCore::AudioOut
diff --git a/src/audio_core/out/audio_out_system.h b/src/audio_core/out/audio_out_system.h
new file mode 100644
index 0000000000..4ca2f3417f
--- /dev/null
+++ b/src/audio_core/out/audio_out_system.h
@@ -0,0 +1,257 @@
+// 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;
+}
+
+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();
+
+    /**
+     * 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);
+
+    /**
+     * Initialize this system.
+     *
+     * @param device_name             - The name of the requested output device.
+     * @param in_params               - Input parameters, see AudioOutParameter.
+     * @param handle                  - Unused.
+     * @param applet_resource_user_id - Unused.
+     * @return Result code.
+     */
+    Result Initialize(std::string& device_name, const AudioOutParameter& in_params, u32 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 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);
+
+    /**
+     * Get the maximum number of usable buffers (default 32).
+     *
+     * @return The number of buffers.
+     */
+    u32 GetBufferCount();
+
+    /**
+     * Get the total number of samples played by this system.
+     *
+     * @return The number of samples.
+     */
+    u64 GetPlayedSampleCount() const;
+
+private:
+    /// Core system
+    Core::System& system;
+    /// (Unused)
+    u32 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
diff --git a/src/audio_core/renderer/adsp/adsp.cpp b/src/audio_core/renderer/adsp/adsp.cpp
new file mode 100644
index 0000000000..e05a22d863
--- /dev/null
+++ b/src/audio_core/renderer/adsp/adsp.cpp
@@ -0,0 +1,118 @@
+// 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/core_timing_util.h"
+#include "core/memory.h"
+
+namespace AudioCore::AudioRenderer::ADSP {
+
+ADSP::ADSP(Core::System& system_, Sink::Sink& sink_)
+    : system{system_}, memory{system.Memory()}, 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, 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
diff --git a/src/audio_core/renderer/adsp/adsp.h b/src/audio_core/renderer/adsp/adsp.h
new file mode 100644
index 0000000000..4dfcef4a51
--- /dev/null
+++ b/src/audio_core/renderer/adsp/adsp.h
@@ -0,0 +1,173 @@
+// 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.
+     *
+     * @return True if started or already running, otherwise false.
+     */
+    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, 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
diff --git a/src/audio_core/renderer/adsp/audio_renderer.cpp b/src/audio_core/renderer/adsp/audio_renderer.cpp
new file mode 100644
index 0000000000..3967ccfe69
--- /dev/null
+++ b/src/audio_core/renderer/adsp/audio_renderer.cpp
@@ -0,0 +1,226 @@
+// 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"
+#include "core/core_timing_util.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 s32 session_id) {
+    return command_buffers[session_id];
+}
+
+void AudioRenderer_Mailbox::SetCommandBuffer(const u32 session_id, 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::thread(&AudioRenderer::ThreadFunc, this);
+    for (auto& stream : streams) {
+        stream->Start();
+    }
+    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);
+    }
+}
+
+void AudioRenderer::ThreadFunc() {
+    constexpr char name[]{"yuzu:AudioRenderer"};
+    MicroProfileOnThreadCreate(name);
+    Common::SetCurrentThreadName(name);
+    Common::SetCurrentThreadPriority(Common::ThreadPriority::Critical);
+    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);
+
+    constexpr u64 max_process_time{2'304'000ULL};
+
+    while (true) {
+        auto message{mailbox->ADSPWaitMessage()};
+        switch (message) {
+        case RenderMessage::AudioRenderer_Shutdown:
+            mailbox->ADSPSendMessage(RenderMessage::AudioRenderer_Shutdown);
+            return;
+
+        case RenderMessage::AudioRenderer_Render: {
+            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, initalize 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 -
+                                   Core::Timing::CyclesToNs(render_times_taken[0]).count();
+                        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);
+
+                    // Process the command list
+                    {
+                        MICROPROFILE_SCOPE(Audio_Renderer);
+                        render_times_taken[index] =
+                            command_list_processor.Process(index) - start_time;
+                    }
+
+                    if (index == 0) {
+                        auto stream{command_list_processor.GetOutputSinkStream()};
+                        system.AudioCore().SetStreamQueue(stream->GetQueueSize());
+                    }
+
+                    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
diff --git a/src/audio_core/renderer/adsp/audio_renderer.h b/src/audio_core/renderer/adsp/audio_renderer.h
new file mode 100644
index 0000000000..b6ced9d2b0
--- /dev/null
+++ b/src/audio_core/renderer/adsp/audio_renderer.h
@@ -0,0 +1,203 @@
+// 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/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(s32 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, 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 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();
+
+    /**
+     * Creates the streams which will receive the processed samples.
+     */
+    void CreateSinkStreams();
+
+    /// Core system
+    Core::System& system;
+    /// Main thread
+    std::thread 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
diff --git a/src/audio_core/renderer/adsp/command_buffer.h b/src/audio_core/renderer/adsp/command_buffer.h
new file mode 100644
index 0000000000..880b279d8f
--- /dev/null
+++ b/src/audio_core/renderer/adsp/command_buffer.h
@@ -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
diff --git a/src/audio_core/renderer/adsp/command_list_processor.cpp b/src/audio_core/renderer/adsp/command_list_processor.cpp
new file mode 100644
index 0000000000..e3bf2d7ecd
--- /dev/null
+++ b/src/audio_core/renderer/adsp/command_list_processor.cpp
@@ -0,0 +1,109 @@
+// 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/core_timing_util.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->Memory();
+    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
diff --git a/src/audio_core/renderer/adsp/command_list_processor.h b/src/audio_core/renderer/adsp/command_list_processor.h
new file mode 100644
index 0000000000..3f99173e3e
--- /dev/null
+++ b/src/audio_core/renderer/adsp/command_list_processor.h
@@ -0,0 +1,118 @@
+// 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 index - Index of the current command list.
+     * @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 alloted
+    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
diff --git a/src/audio_core/renderer/audio_device.cpp b/src/audio_core/renderer/audio_device.cpp
new file mode 100644
index 0000000000..d5886e55e4
--- /dev/null
+++ b/src/audio_core/renderer/audio_device.cpp
@@ -0,0 +1,52 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#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::AudioRenderer {
+
+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::vector<AudioDeviceName>& out_buffer,
+                                     const size_t max_count) {
+    std::span<AudioDeviceName> names{};
+
+    if (CheckFeatureSupported(SupportTags::AudioUsbDeviceOutput, user_revision)) {
+        names = usb_device_names;
+    } else {
+        names = device_names;
+    }
+
+    u32 out_count{static_cast<u32>(std::min(max_count, names.size()))};
+    for (u32 i = 0; i < out_count; i++) {
+        out_buffer.push_back(names[i]);
+    }
+    return out_count;
+}
+
+u32 AudioDevice::ListAudioOutputDeviceName(std::vector<AudioDeviceName>& out_buffer,
+                                           const size_t max_count) {
+    u32 out_count{static_cast<u32>(std::min(max_count, output_device_names.size()))};
+
+    for (u32 i = 0; i < out_count; i++) {
+        out_buffer.push_back(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) {
+    return output_sink.GetDeviceVolume();
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/audio_device.h b/src/audio_core/renderer/audio_device.h
new file mode 100644
index 0000000000..1f449f2612
--- /dev/null
+++ b/src/audio_core/renderer/audio_device.h
@@ -0,0 +1,88 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <span>
+
+#include "audio_core/audio_render_manager.h"
+
+namespace Core {
+class System;
+}
+
+namespace AudioCore {
+namespace Sink {
+class Sink;
+}
+
+namespace AudioRenderer {
+/**
+ * An interface to an output audio device available to the Switch.
+ */
+class AudioDevice {
+public:
+    struct AudioDeviceName {
+        std::array<char, 0x100> name;
+
+        AudioDeviceName(const char* name_) {
+            std::strncpy(name.data(), name_, name.size());
+        }
+    };
+
+    std::array<AudioDeviceName, 4> usb_device_names{"AudioStereoJackOutput",
+                                                    "AudioBuiltInSpeakerOutput", "AudioTvOutput",
+                                                    "AudioUsbDeviceOutput"};
+    std::array<AudioDeviceName, 3> device_names{"AudioStereoJackOutput",
+                                                "AudioBuiltInSpeakerOutput", "AudioTvOutput"};
+    std::array<AudioDeviceName, 3> output_device_names{"AudioBuiltInSpeakerOutput", "AudioTvOutput",
+                                                       "AudioExternalOutput"};
+
+    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.
+     * @param max_count  - Maximum number of devices to write (count of out_buffer).
+     * @return Number of device names written.
+     */
+    u32 ListAudioDeviceName(std::vector<AudioDeviceName>& out_buffer, size_t max_count);
+
+    /**
+     * Get a list of the available output devices.
+     * Different to above somehow...
+     *
+     * @param out_buffer - Output buffer to write the available device names.
+     * @param max_count  - Maximum number of devices to write (count of out_buffer).
+     * @return Number of device names written.
+     */
+    u32 ListAudioOutputDeviceName(std::vector<AudioDeviceName>& out_buffer, size_t max_count);
+
+    /**
+     * 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);
+
+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 AudioRenderer
+} // namespace AudioCore
diff --git a/src/audio_core/renderer/audio_renderer.cpp b/src/audio_core/renderer/audio_renderer.cpp
new file mode 100644
index 0000000000..51aa17599e
--- /dev/null
+++ b/src/audio_core/renderer/audio_renderer.cpp
@@ -0,0 +1,67 @@
+// 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_transfer_memory.h"
+#include "core/hle/service/audio/errors.h"
+
+namespace AudioCore::AudioRenderer {
+
+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, const u32 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::ERR_MAXIMUM_SESSIONS_REACHED;
+        }
+        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::AudioRenderer
diff --git a/src/audio_core/renderer/audio_renderer.h b/src/audio_core/renderer/audio_renderer.h
new file mode 100644
index 0000000000..90c6f97279
--- /dev/null
+++ b/src/audio_core/renderer/audio_renderer.h
@@ -0,0 +1,97 @@
+// 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;
+}
+
+namespace AudioCore {
+struct AudioRendererParameterInternal;
+
+namespace AudioRenderer {
+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 AudioRenderer::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. Unused.
+     * @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,
+                      u32 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 AudioRenderer
+} // namespace AudioCore
diff --git a/src/audio_core/renderer/behavior/behavior_info.cpp b/src/audio_core/renderer/behavior/behavior_info.cpp
new file mode 100644
index 0000000000..c5d4d66d8f
--- /dev/null
+++ b/src/audio_core/renderer/behavior/behavior_info.cpp
@@ -0,0 +1,191 @@
+// 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::AudioRenderer {
+
+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(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) {
+    auto error_count_{std::min(error_count, MaxErrors)};
+    std::memset(out_errors.data(), 0, MaxErrors * sizeof(ErrorInfo));
+
+    for (size_t i = 0; i < error_count_; i++) {
+        out_errors[i] = errors[i];
+    }
+    out_count = error_count_;
+}
+
+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::AudioRenderer
diff --git a/src/audio_core/renderer/behavior/behavior_info.h b/src/audio_core/renderer/behavior/behavior_info.h
new file mode 100644
index 0000000000..7333c297f3
--- /dev/null
+++ b/src/audio_core/renderer/behavior/behavior_info.h
@@ -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::AudioRenderer {
+/**
+ * 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(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);
+
+    /**
+     * 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 calcualting 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::AudioRenderer::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::AudioRenderer::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::AudioRenderer
diff --git a/src/audio_core/renderer/behavior/info_updater.cpp b/src/audio_core/renderer/behavior/info_updater.cpp
new file mode 100644
index 0000000000..06a37e1a66
--- /dev/null
+++ b/src/audio_core/renderer/behavior/info_updater.cpp
@@ -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::AudioRenderer {
+
+InfoUpdater::InfoUpdater(std::span<const u8> input_, std::span<u8> output_,
+                         const u32 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::ERR_INVALID_UPDATE_DATA;
+    }
+
+    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++) {
+                std::memset(voice_states[channel], 0, sizeof(VoiceState));
+            }
+        }
+
+        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::ERR_INVALID_UPDATE_DATA;
+    }
+
+    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::ERR_INVALID_UPDATE_DATA;
+    }
+
+    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::ERR_INVALID_UPDATE_DATA;
+    }
+
+    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::ERR_INVALID_UPDATE_DATA;
+    }
+
+    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::ERR_INVALID_UPDATE_DATA;
+            }
+        }
+    }
+
+    if (total_buffer_count > mix_buffer_count) {
+        return Service::Audio::ERR_INVALID_UPDATE_DATA;
+    }
+
+    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::ERR_INVALID_UPDATE_DATA;
+            }
+        } 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::ERR_INVALID_UPDATE_DATA;
+    }
+
+    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::ERR_INVALID_UPDATE_DATA;
+    }
+
+    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::ERR_INVALID_UPDATE_DATA;
+        }
+    }
+
+    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::ERR_INVALID_UPDATE_DATA;
+    }
+
+    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::ERR_INVALID_UPDATE_DATA;
+    }
+
+    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::ERR_INVALID_UPDATE_DATA;
+    }
+
+    if (in_params->revision != behaviour_.GetUserRevision()) {
+        return Service::Audio::ERR_INVALID_UPDATE_DATA;
+    }
+
+    behaviour_.ClearError();
+    behaviour_.UpdateFlags(in_params->flags);
+
+    if (in_header->behaviour_size != sizeof(BehaviorInfo::InParameter)) {
+        return Service::Audio::ERR_INVALID_UPDATE_DATA;
+    }
+
+    input += sizeof(BehaviorInfo::InParameter);
+    return ResultSuccess;
+}
+
+Result InfoUpdater::UpdateErrorInfo(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::ERR_INVALID_UPDATE_DATA;
+    }
+
+    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::ERR_INVALID_UPDATE_DATA;
+    } else if (CpuAddr(output) - CpuAddr(output_origin.data()) != expected_output_size) {
+        return Service::Audio::ERR_INVALID_UPDATE_DATA;
+    }
+    return ResultSuccess;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/behavior/info_updater.h b/src/audio_core/renderer/behavior/info_updater.h
new file mode 100644
index 0000000000..f0b445d9c0
--- /dev/null
+++ b/src/audio_core/renderer/behavior/info_updater.h
@@ -0,0 +1,205 @@
+// 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 AudioCore::AudioRenderer {
+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, u32 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(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
+    u32 process_handle;
+    /// Behaviour
+    BehaviorInfo& behaviour;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/command_buffer.cpp b/src/audio_core/renderer/command/command_buffer.cpp
new file mode 100644
index 0000000000..40074cf14a
--- /dev/null
+++ b/src/audio_core/renderer/command/command_buffer.cpp
@@ -0,0 +1,714 @@
+// 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::AudioRenderer {
+
+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())};
+
+    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_count * sizeof(s32));
+
+    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;
+
+    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),
+                             parameter.input_count * state.upsampler_info->sample_count};
+    } else {
+        cmd.sample_buffer = samples_buffer;
+    }
+
+    cmd.input_count = parameter.input_count;
+    for (u32 i = 0; i < parameter.input_count; i++) {
+        cmd.inputs[i] = buffer_offset + parameter.inputs[i];
+    }
+
+    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::AudioRenderer
diff --git a/src/audio_core/renderer/command/command_buffer.h b/src/audio_core/renderer/command/command_buffer.h
new file mode 100644
index 0000000000..496b0e50a3
--- /dev/null
+++ b/src/audio_core/renderer/command/command_buffer.h
@@ -0,0 +1,466 @@
+// 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::AudioRenderer {
+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 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 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.
+     * @session_id          - System session id this command is generated from.
+     * @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::AudioRenderer
diff --git a/src/audio_core/renderer/command/command_generator.cpp b/src/audio_core/renderer/command/command_generator.cpp
new file mode 100644
index 0000000000..2ea50d1289
--- /dev/null
+++ b/src/audio_core/renderer/command/command_generator.cpp
@@ -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::AudioRenderer {
+
+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.GetDesintationData(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()) {
+                        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.GetDesintationData(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.GetDesintationData(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.GetDesintationData(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.GetDesintationData(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.GetDesintationData(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::AudioRenderer
diff --git a/src/audio_core/renderer/command/command_generator.h b/src/audio_core/renderer/command/command_generator.h
new file mode 100644
index 0000000000..d80d9b0d8b
--- /dev/null
+++ b/src/audio_core/renderer/command/command_generator.h
@@ -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 AudioRenderer {
+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_base - 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_base - 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_base - 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_base - 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_base - 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_base - Compressor effect info.
+     * @param node_id          - Node id of the mix this command is generated for.
+     */
+    void GenerateCompressorCommand(const s16 buffer_offset, EffectInfoBase& effect_info,
+                                   const 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 buffer_offset - Base mix buffer offset to use.
+     * @param sink_info     - Sink info to generate the commands from.
+     */
+    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 AudioRenderer
+} // namespace AudioCore
diff --git a/src/audio_core/renderer/command/command_list_header.h b/src/audio_core/renderer/command/command_list_header.h
new file mode 100644
index 0000000000..988530b1f4
--- /dev/null
+++ b/src/audio_core/renderer/command/command_list_header.h
@@ -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::AudioRenderer {
+
+struct CommandListHeader {
+    u64 buffer_size;
+    u32 command_count;
+    std::span<s32> samples_buffer;
+    s16 buffer_count;
+    u32 sample_count;
+    u32 sample_rate;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/command_processing_time_estimator.cpp b/src/audio_core/renderer/command/command_processing_time_estimator.cpp
new file mode 100644
index 0000000000..3091f587ac
--- /dev/null
+++ b/src/audio_core/renderer/command/command_processing_time_estimator.cpp
@@ -0,0 +1,3620 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/command/command_processing_time_estimator.h"
+
+namespace AudioCore::AudioRenderer {
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(
+    const PcmInt16DataSourceVersion1Command& command) const {
+    return static_cast<u32>(command.pitch * 0.25f * 1.2f);
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(
+    const PcmInt16DataSourceVersion2Command& command) const {
+    return static_cast<u32>(command.pitch * 0.25f * 1.2f);
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(
+    [[maybe_unused]] const PcmFloatDataSourceVersion1Command& command) const {
+    return 0;
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(
+    [[maybe_unused]] const PcmFloatDataSourceVersion2Command& command) const {
+    return 0;
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(
+    const AdpcmDataSourceVersion1Command& command) const {
+    return static_cast<u32>(command.pitch * 0.25f * 1.2f);
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(
+    const AdpcmDataSourceVersion2Command& command) const {
+    return static_cast<u32>(command.pitch * 0.25f * 1.2f);
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(
+    [[maybe_unused]] const VolumeCommand& command) const {
+    return static_cast<u32>((static_cast<f32>(sample_count) * 8.8f) * 1.2f);
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(
+    [[maybe_unused]] const VolumeRampCommand& command) const {
+    return static_cast<u32>((static_cast<f32>(sample_count) * 9.8f) * 1.2f);
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(
+    [[maybe_unused]] const BiquadFilterCommand& command) const {
+    return static_cast<u32>((static_cast<f32>(sample_count) * 58.0f) * 1.2f);
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(
+    [[maybe_unused]] const MixCommand& command) const {
+    return static_cast<u32>((static_cast<f32>(sample_count) * 10.0f) * 1.2f);
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(
+    [[maybe_unused]] const MixRampCommand& command) const {
+    return static_cast<u32>((static_cast<f32>(sample_count) * 14.4f) * 1.2f);
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(const MixRampGroupedCommand& command) const {
+    u32 count{0};
+    for (u32 i = 0; i < command.buffer_count; i++) {
+        if (command.volumes[i] != 0.0f || command.prev_volumes[i] != 0.0f) {
+            count++;
+        }
+    }
+
+    return static_cast<u32>(((static_cast<f32>(sample_count) * 14.4f) * 1.2f) *
+                            static_cast<f32>(count));
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(
+    [[maybe_unused]] const DepopPrepareCommand& command) const {
+    return 1080;
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(
+    const DepopForMixBuffersCommand& command) const {
+    return static_cast<u32>((static_cast<f32>(sample_count) * 8.9f) *
+                            static_cast<f32>(command.count));
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(const DelayCommand& command) const {
+    return static_cast<u32>((static_cast<f32>(sample_count) * command.parameter.channel_count) *
+                            202.5f);
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(
+    [[maybe_unused]] const UpsampleCommand& command) const {
+    return 357915;
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(
+    [[maybe_unused]] const DownMix6chTo2chCommand& command) const {
+    return 16108;
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(const AuxCommand& command) const {
+    if (command.enabled) {
+        return 15956;
+    }
+    return 3765;
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(
+    [[maybe_unused]] const DeviceSinkCommand& command) const {
+    return 10042;
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(
+    [[maybe_unused]] const CircularBufferSinkCommand& command) const {
+    return 55;
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(const ReverbCommand& command) const {
+    if (command.enabled) {
+        return static_cast<u32>(
+            (command.parameter.channel_count * static_cast<f32>(sample_count) * 750) * 1.2f);
+    }
+    return 0;
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(const I3dl2ReverbCommand& command) const {
+    if (command.enabled) {
+        return static_cast<u32>(
+            (command.parameter.channel_count * static_cast<f32>(sample_count) * 530) * 1.2f);
+    }
+    return 0;
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(
+    [[maybe_unused]] const PerformanceCommand& command) const {
+    return 1454;
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(
+    [[maybe_unused]] const ClearMixBufferCommand& command) const {
+    return static_cast<u32>(
+        ((static_cast<f32>(sample_count) * 0.83f) * static_cast<f32>(buffer_count)) * 1.2f);
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(
+    [[maybe_unused]] const CopyMixBufferCommand& command) const {
+    return 0;
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(
+    [[maybe_unused]] const LightLimiterVersion1Command& command) const {
+    return 0;
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(
+    [[maybe_unused]] const LightLimiterVersion2Command& command) const {
+    return 0;
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(
+    [[maybe_unused]] const MultiTapBiquadFilterCommand& command) const {
+    return 0;
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(
+    [[maybe_unused]] const CaptureCommand& command) const {
+    return 0;
+}
+
+u32 CommandProcessingTimeEstimatorVersion1::Estimate(
+    [[maybe_unused]] const CompressorCommand& command) const {
+    return 0;
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(
+    const PcmInt16DataSourceVersion1Command& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(
+            (static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+                (command.pitch * 2.0f) * 749.269f +
+            6138.94f);
+    case 240:
+        return static_cast<u32>(
+            (static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+                (command.pitch * 2.0f) * 1195.456f +
+            7797.047f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(
+    const PcmInt16DataSourceVersion2Command& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(
+            (static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+                (command.pitch * 2.0f) * 749.269f +
+            6138.94f);
+    case 240:
+        return static_cast<u32>(
+            (static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+                (command.pitch * 2.0f) * 1195.456f +
+            7797.047f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(
+    const PcmFloatDataSourceVersion1Command& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(
+            (static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+                (command.pitch * 2.0f) * 749.269f +
+            6138.94f);
+    case 240:
+        return static_cast<u32>(
+            (static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+                (command.pitch * 2.0f) * 1195.456f +
+            7797.047f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(
+    const PcmFloatDataSourceVersion2Command& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(
+            (static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+                (command.pitch * 2.0f) * 749.269f +
+            6138.94f);
+    case 240:
+        return static_cast<u32>(
+            (static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+                (command.pitch * 2.0f) * 1195.456f +
+            7797.047f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(
+    const AdpcmDataSourceVersion1Command& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(
+            (static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+                (command.pitch * 2.0f) * 2125.588f +
+            9039.47f);
+    case 240:
+        return static_cast<u32>(
+            (static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+                (command.pitch * 2.0f) * 3564.088 +
+            6225.471);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(
+    const AdpcmDataSourceVersion2Command& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(
+            (static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+                (command.pitch * 2.0f) * 2125.588f +
+            9039.47f);
+    case 240:
+        return static_cast<u32>(
+            (static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+                (command.pitch * 2.0f) * 3564.088 +
+            6225.471);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(
+    [[maybe_unused]] const VolumeCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(1280.3f);
+    case 240:
+        return static_cast<u32>(1737.8f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(
+    [[maybe_unused]] const VolumeRampCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(1403.9f);
+    case 240:
+        return static_cast<u32>(1884.3f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(
+    [[maybe_unused]] const BiquadFilterCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(4813.2f);
+    case 240:
+        return static_cast<u32>(6915.4f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(
+    [[maybe_unused]] const MixCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(1342.2f);
+    case 240:
+        return static_cast<u32>(1833.2f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(
+    [[maybe_unused]] const MixRampCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(1859.0f);
+    case 240:
+        return static_cast<u32>(2286.1f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(const MixRampGroupedCommand& command) const {
+    u32 count{0};
+    for (u32 i = 0; i < command.buffer_count; i++) {
+        if (command.volumes[i] != 0.0f || command.prev_volumes[i] != 0.0f) {
+            count++;
+        }
+    }
+
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>((static_cast<f32>(sample_count) * 7.245f) *
+                                static_cast<f32>(count));
+    case 240:
+        return static_cast<u32>((static_cast<f32>(sample_count) * 7.245f) *
+                                static_cast<f32>(count));
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(
+    [[maybe_unused]] const DepopPrepareCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(306.62f);
+    case 240:
+        return static_cast<u32>(293.22f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(
+    [[maybe_unused]] const DepopForMixBuffersCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(762.96f);
+    case 240:
+        return static_cast<u32>(726.96f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(const DelayCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(41635.555f);
+            case 2:
+                return static_cast<u32>(97861.211f);
+            case 4:
+                return static_cast<u32>(192515.516f);
+            case 6:
+                return static_cast<u32>(301755.969f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(578.529f);
+        case 2:
+            return static_cast<u32>(663.064f);
+        case 4:
+            return static_cast<u32>(703.983f);
+        case 6:
+            return static_cast<u32>(760.032f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    case 240:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(8770.345f);
+            case 2:
+                return static_cast<u32>(25741.18f);
+            case 4:
+                return static_cast<u32>(47551.168f);
+            case 6:
+                return static_cast<u32>(81629.219f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(521.283f);
+        case 2:
+            return static_cast<u32>(585.396f);
+        case 4:
+            return static_cast<u32>(629.884f);
+        case 6:
+            return static_cast<u32>(713.57f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(
+    [[maybe_unused]] const UpsampleCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(292000.0f);
+    case 240:
+        return static_cast<u32>(0.0f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(
+    [[maybe_unused]] const DownMix6chTo2chCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(10009.0f);
+    case 240:
+        return static_cast<u32>(14577.0f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(const AuxCommand& command) const {
+    // Is this function bugged, returning the wrong time?
+    // Surely the larger time should be returned when enabled...
+    // CMP W8, #0
+    // MOV W8, #0x60;  // 489.163f
+    // MOV W10, #0x64; // 7177.936f
+    // CSEL X8, X10, X8, EQ
+
+    switch (sample_count) {
+    case 160:
+        if (command.enabled) {
+            return static_cast<u32>(489.163f);
+        }
+        return static_cast<u32>(7177.936f);
+    case 240:
+        if (command.enabled) {
+            return static_cast<u32>(485.562f);
+        }
+        return static_cast<u32>(9499.822f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(const DeviceSinkCommand& command) const {
+    switch (command.input_count) {
+    case 2:
+        switch (sample_count) {
+        case 160:
+            return static_cast<u32>(9261.545f);
+        case 240:
+            return static_cast<u32>(9336.054f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+            return 0;
+        }
+    case 6:
+        switch (sample_count) {
+        case 160:
+            return static_cast<u32>(9336.054f);
+        case 240:
+            return static_cast<u32>(9566.728f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+            return 0;
+        }
+    default:
+        LOG_ERROR(Service_Audio, "Invalid input count {}", command.input_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(
+    const CircularBufferSinkCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(static_cast<f32>(command.input_count) * 853.629f + 1284.517f);
+    case 240:
+        return static_cast<u32>(static_cast<f32>(command.input_count) * 1726.021f + 1369.683f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(const ReverbCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(97192.227f);
+            case 2:
+                return static_cast<u32>(103278.555f);
+            case 4:
+                return static_cast<u32>(109579.039f);
+            case 6:
+                return static_cast<u32>(115065.438f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(492.009f);
+        case 2:
+            return static_cast<u32>(554.463f);
+        case 4:
+            return static_cast<u32>(595.864f);
+        case 6:
+            return static_cast<u32>(656.617f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    case 240:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(136463.641f);
+            case 2:
+                return static_cast<u32>(145749.047f);
+            case 4:
+                return static_cast<u32>(154796.938f);
+            case 6:
+                return static_cast<u32>(161968.406f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(495.789f);
+        case 2:
+            return static_cast<u32>(527.163f);
+        case 4:
+            return static_cast<u32>(598.752f);
+        case 6:
+            return static_cast<u32>(666.025f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(const I3dl2ReverbCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(138836.484f);
+            case 2:
+                return static_cast<u32>(135428.172f);
+            case 4:
+                return static_cast<u32>(199181.844f);
+            case 6:
+                return static_cast<u32>(247345.906f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(718.704f);
+        case 2:
+            return static_cast<u32>(751.296f);
+        case 4:
+            return static_cast<u32>(797.464f);
+        case 6:
+            return static_cast<u32>(867.426f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    case 240:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(199952.734f);
+            case 2:
+                return static_cast<u32>(195199.5f);
+            case 4:
+                return static_cast<u32>(290575.875f);
+            case 6:
+                return static_cast<u32>(363494.531f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(534.24f);
+        case 2:
+            return static_cast<u32>(570.874f);
+        case 4:
+            return static_cast<u32>(660.933f);
+        case 6:
+            return static_cast<u32>(694.596f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(
+    [[maybe_unused]] const PerformanceCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(489.35f);
+    case 240:
+        return static_cast<u32>(491.18f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(
+    [[maybe_unused]] const ClearMixBufferCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(static_cast<f32>(buffer_count) * 260.4f + 139.65f);
+    case 240:
+        return static_cast<u32>(static_cast<f32>(buffer_count) * 668.85f + 193.2f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(
+    [[maybe_unused]] const CopyMixBufferCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(836.32f);
+    case 240:
+        return static_cast<u32>(1000.9f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(
+    [[maybe_unused]] const LightLimiterVersion1Command& command) const {
+    return 0;
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(
+    [[maybe_unused]] const LightLimiterVersion2Command& command) const {
+    return 0;
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(
+    [[maybe_unused]] const MultiTapBiquadFilterCommand& command) const {
+    return 0;
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(
+    [[maybe_unused]] const CaptureCommand& command) const {
+    return 0;
+}
+
+u32 CommandProcessingTimeEstimatorVersion2::Estimate(
+    [[maybe_unused]] const CompressorCommand& command) const {
+    return 0;
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(
+    const PcmInt16DataSourceVersion1Command& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(
+            ((static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+             (command.pitch * 0.000030518f)) *
+                427.52f +
+            6329.442f);
+    case 240:
+        return static_cast<u32>(
+            ((static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+             (command.pitch * 0.000030518f)) *
+                710.143f +
+            7853.286f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(
+    const PcmInt16DataSourceVersion2Command& command) const {
+    switch (sample_count) {
+    case 160:
+        switch (command.src_quality) {
+        case SrcQuality::Medium:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        427.52f +
+                                    6329.442f);
+        case SrcQuality::High:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        371.876f +
+                                    8049.415f);
+        case SrcQuality::Low:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        423.43f +
+                                    5062.659f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid SRC quality {}",
+                      static_cast<u32>(command.src_quality));
+            return 0;
+        }
+
+    case 240:
+        switch (command.src_quality) {
+        case SrcQuality::Medium:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        710.143f +
+                                    7853.286f);
+        case SrcQuality::High:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        610.487f +
+                                    10138.842f);
+        case SrcQuality::Low:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        676.722f +
+                                    5810.962f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid SRC quality {}",
+                      static_cast<u32>(command.src_quality));
+            return 0;
+        }
+
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(
+    const PcmFloatDataSourceVersion1Command& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(
+            ((static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+             (command.pitch * 0.000030518f)) *
+                1672.026f +
+            7681.211f);
+    case 240:
+        return static_cast<u32>(
+            ((static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+             (command.pitch * 0.000030518f)) *
+                2550.414f +
+            9663.969f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(
+    const PcmFloatDataSourceVersion2Command& command) const {
+    switch (sample_count) {
+    case 160:
+        switch (command.src_quality) {
+        case SrcQuality::Medium:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        1672.026f +
+                                    7681.211f);
+        case SrcQuality::High:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        1672.982f +
+                                    9038.011f);
+        case SrcQuality::Low:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        1673.216f +
+                                    6027.577f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid SRC quality {}",
+                      static_cast<u32>(command.src_quality));
+            return 0;
+        }
+
+    case 240:
+        switch (command.src_quality) {
+        case SrcQuality::Medium:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        2550.414f +
+                                    9663.969f);
+        case SrcQuality::High:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        2522.303f +
+                                    11758.571f);
+        case SrcQuality::Low:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        2537.061f +
+                                    7369.309f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid SRC quality {}",
+                      static_cast<u32>(command.src_quality));
+            return 0;
+        }
+
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(
+    const AdpcmDataSourceVersion1Command& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(
+            ((static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+             (command.pitch * 0.000030518f)) *
+                1827.665f +
+            7913.808f);
+    case 240:
+        return static_cast<u32>(
+            ((static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+             (command.pitch * 0.000030518f)) *
+                2756.372f +
+            9736.702f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(
+    const AdpcmDataSourceVersion2Command& command) const {
+    switch (sample_count) {
+    case 160:
+        switch (command.src_quality) {
+        case SrcQuality::Medium:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        1827.665f +
+                                    7913.808f);
+        case SrcQuality::High:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        1829.285f +
+                                    9607.814f);
+        case SrcQuality::Low:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        1824.609f +
+                                    6517.476f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid SRC quality {}",
+                      static_cast<u32>(command.src_quality));
+            return 0;
+        }
+
+    case 240:
+        switch (command.src_quality) {
+        case SrcQuality::Medium:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        2756.372f +
+                                    9736.702f);
+        case SrcQuality::High:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        2731.308f +
+                                    12154.379f);
+        case SrcQuality::Low:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        2732.152f +
+                                    7929.442f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid SRC quality {}",
+                      static_cast<u32>(command.src_quality));
+            return 0;
+        }
+
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(
+    [[maybe_unused]] const VolumeCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(1311.1f);
+    case 240:
+        return static_cast<u32>(1713.6f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(
+    [[maybe_unused]] const VolumeRampCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(1425.3f);
+    case 240:
+        return static_cast<u32>(1700.0f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(
+    [[maybe_unused]] const BiquadFilterCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(4173.2f);
+    case 240:
+        return static_cast<u32>(5585.1f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(
+    [[maybe_unused]] const MixCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(1402.8f);
+    case 240:
+        return static_cast<u32>(1853.2f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(
+    [[maybe_unused]] const MixRampCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(1968.7f);
+    case 240:
+        return static_cast<u32>(2459.4f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(const MixRampGroupedCommand& command) const {
+    u32 count{0};
+    for (u32 i = 0; i < command.buffer_count; i++) {
+        if (command.volumes[i] != 0.0f || command.prev_volumes[i] != 0.0f) {
+            count++;
+        }
+    }
+
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>((static_cast<f32>(sample_count) * 6.708f) *
+                                static_cast<f32>(count));
+    case 240:
+        return static_cast<u32>((static_cast<f32>(sample_count) * 6.443f) *
+                                static_cast<f32>(count));
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(
+    [[maybe_unused]] const DepopPrepareCommand& command) const {
+    return 0;
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(
+    [[maybe_unused]] const DepopForMixBuffersCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(739.64f);
+    case 240:
+        return static_cast<u32>(910.97f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(const DelayCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(8929.042f);
+            case 2:
+                return static_cast<u32>(25500.75f);
+            case 4:
+                return static_cast<u32>(47759.617f);
+            case 6:
+                return static_cast<u32>(82203.07f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(1295.206f);
+        case 2:
+            return static_cast<u32>(1213.6f);
+        case 4:
+            return static_cast<u32>(942.028f);
+        case 6:
+            return static_cast<u32>(1001.553f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    case 240:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(11941.051f);
+            case 2:
+                return static_cast<u32>(37197.371f);
+            case 4:
+                return static_cast<u32>(69749.836f);
+            case 6:
+                return static_cast<u32>(120042.398f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(997.668f);
+        case 2:
+            return static_cast<u32>(977.634f);
+        case 4:
+            return static_cast<u32>(792.309f);
+        case 6:
+            return static_cast<u32>(875.427f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(
+    [[maybe_unused]] const UpsampleCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(312990.0f);
+    case 240:
+        return static_cast<u32>(0.0f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(
+    [[maybe_unused]] const DownMix6chTo2chCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(9949.7f);
+    case 240:
+        return static_cast<u32>(14679.0f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(const AuxCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        if (command.enabled) {
+            return static_cast<u32>(7182.136f);
+        }
+        return static_cast<u32>(472.111f);
+    case 240:
+        if (command.enabled) {
+            return static_cast<u32>(9435.961f);
+        }
+        return static_cast<u32>(462.619f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(const DeviceSinkCommand& command) const {
+    switch (command.input_count) {
+    case 2:
+        switch (sample_count) {
+        case 160:
+            return static_cast<u32>(8979.956f);
+        case 240:
+            return static_cast<u32>(9221.907f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+            return 0;
+        }
+    case 6:
+        switch (sample_count) {
+        case 160:
+            return static_cast<u32>(9177.903f);
+        case 240:
+            return static_cast<u32>(9725.897f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+            return 0;
+        }
+    default:
+        LOG_ERROR(Service_Audio, "Invalid input count {}", command.input_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(
+    const CircularBufferSinkCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(static_cast<f32>(command.input_count) * 531.069f + 0.0f);
+    case 240:
+        return static_cast<u32>(static_cast<f32>(command.input_count) * 770.257f + 0.0f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(const ReverbCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(81475.055f);
+            case 2:
+                return static_cast<u32>(84975.0f);
+            case 4:
+                return static_cast<u32>(91625.148f);
+            case 6:
+                return static_cast<u32>(95332.266f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(536.298f);
+        case 2:
+            return static_cast<u32>(588.798f);
+        case 4:
+            return static_cast<u32>(643.702f);
+        case 6:
+            return static_cast<u32>(705.999f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    case 240:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(120174.469f);
+            case 2:
+                return static_cast<u32>(125262.219f);
+            case 4:
+                return static_cast<u32>(135751.234f);
+            case 6:
+                return static_cast<u32>(141129.234f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(617.641f);
+        case 2:
+            return static_cast<u32>(659.536f);
+        case 4:
+            return static_cast<u32>(711.438f);
+        case 6:
+            return static_cast<u32>(778.071f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(const I3dl2ReverbCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(116754.984f);
+            case 2:
+                return static_cast<u32>(125912.055f);
+            case 4:
+                return static_cast<u32>(146336.031f);
+            case 6:
+                return static_cast<u32>(165812.656f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(735.0f);
+        case 2:
+            return static_cast<u32>(766.615f);
+        case 4:
+            return static_cast<u32>(834.067f);
+        case 6:
+            return static_cast<u32>(875.437f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    case 240:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(170292.344f);
+            case 2:
+                return static_cast<u32>(183875.625f);
+            case 4:
+                return static_cast<u32>(214696.188f);
+            case 6:
+                return static_cast<u32>(243846.766f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(508.473f);
+        case 2:
+            return static_cast<u32>(582.445f);
+        case 4:
+            return static_cast<u32>(626.419f);
+        case 6:
+            return static_cast<u32>(682.468f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(
+    [[maybe_unused]] const PerformanceCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(498.17f);
+    case 240:
+        return static_cast<u32>(489.42f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(
+    [[maybe_unused]] const ClearMixBufferCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(static_cast<f32>(buffer_count - 1) * 266.645f + 0.0f);
+    case 240:
+        return static_cast<u32>(static_cast<f32>(buffer_count - 1) * 440.681f + 0.0f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(
+    [[maybe_unused]] const CopyMixBufferCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(842.59f);
+    case 240:
+        return static_cast<u32>(986.72f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(
+    const LightLimiterVersion1Command& command) const {
+    switch (sample_count) {
+    case 160:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(21392.383f);
+            case 2:
+                return static_cast<u32>(26829.389f);
+            case 4:
+                return static_cast<u32>(32405.152f);
+            case 6:
+                return static_cast<u32>(52218.586f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(897.004f);
+        case 2:
+            return static_cast<u32>(931.549f);
+        case 4:
+            return static_cast<u32>(975.387f);
+        case 6:
+            return static_cast<u32>(1016.778f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    case 240:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(30555.504f);
+            case 2:
+                return static_cast<u32>(39010.785f);
+            case 4:
+                return static_cast<u32>(48270.18f);
+            case 6:
+                return static_cast<u32>(76711.875f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(874.429f);
+        case 2:
+            return static_cast<u32>(921.553f);
+        case 4:
+            return static_cast<u32>(945.262f);
+        case 6:
+            return static_cast<u32>(992.26f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(
+    const LightLimiterVersion2Command& command) const {
+    switch (sample_count) {
+    case 160:
+        if (command.enabled) {
+            if (command.parameter.statistics_enabled) {
+                switch (command.parameter.channel_count) {
+                case 1:
+                    return static_cast<u32>(23308.928f);
+                case 2:
+                    return static_cast<u32>(29954.062f);
+                case 4:
+                    return static_cast<u32>(35807.477f);
+                case 6:
+                    return static_cast<u32>(58339.773f);
+                default:
+                    LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                              command.parameter.channel_count);
+                    return 0;
+                }
+            }
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(21392.383f);
+            case 2:
+                return static_cast<u32>(26829.389f);
+            case 4:
+                return static_cast<u32>(32405.152f);
+            case 6:
+                return static_cast<u32>(52218.586f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(897.004f);
+        case 2:
+            return static_cast<u32>(931.549f);
+        case 4:
+            return static_cast<u32>(975.387f);
+        case 6:
+            return static_cast<u32>(1016.778f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    case 240:
+        if (command.enabled) {
+            if (command.parameter.statistics_enabled) {
+                switch (command.parameter.channel_count) {
+                case 1:
+                    return static_cast<u32>(33526.121f);
+                case 2:
+                    return static_cast<u32>(43549.355f);
+                case 4:
+                    return static_cast<u32>(52190.281f);
+                case 6:
+                    return static_cast<u32>(85526.516f);
+                default:
+                    LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                              command.parameter.channel_count);
+                    return 0;
+                }
+            }
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(30555.504f);
+            case 2:
+                return static_cast<u32>(39010.785f);
+            case 4:
+                return static_cast<u32>(48270.18f);
+            case 6:
+                return static_cast<u32>(76711.875f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(874.429f);
+        case 2:
+            return static_cast<u32>(921.553f);
+        case 4:
+            return static_cast<u32>(945.262f);
+        case 6:
+            return static_cast<u32>(992.26f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(
+    [[maybe_unused]] const MultiTapBiquadFilterCommand& command) const {
+    return 0;
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(
+    [[maybe_unused]] const CaptureCommand& command) const {
+    return 0;
+}
+
+u32 CommandProcessingTimeEstimatorVersion3::Estimate(
+    [[maybe_unused]] const CompressorCommand& command) const {
+    return 0;
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(
+    const PcmInt16DataSourceVersion1Command& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(
+            ((static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+             (command.pitch * 0.000030518f)) *
+                427.52f +
+            6329.442f);
+    case 240:
+        return static_cast<u32>(
+            ((static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+             (command.pitch * 0.000030518f)) *
+                710.143f +
+            7853.286f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(
+    const PcmInt16DataSourceVersion2Command& command) const {
+    switch (sample_count) {
+    case 160:
+        switch (command.src_quality) {
+        case SrcQuality::Medium:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        427.52f +
+                                    6329.442f);
+        case SrcQuality::High:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        371.876f +
+                                    8049.415f);
+        case SrcQuality::Low:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        423.43f +
+                                    5062.659f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid SRC quality {}",
+                      static_cast<u32>(command.src_quality));
+            return 0;
+        }
+
+    case 240:
+        switch (command.src_quality) {
+        case SrcQuality::Medium:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        710.143f +
+                                    7853.286f);
+        case SrcQuality::High:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        610.487f +
+                                    10138.842f);
+        case SrcQuality::Low:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        676.722f +
+                                    5810.962f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid SRC quality {}",
+                      static_cast<u32>(command.src_quality));
+            return 0;
+        }
+
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(
+    const PcmFloatDataSourceVersion1Command& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(
+            ((static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+             (command.pitch * 0.000030518f)) *
+                1672.026f +
+            7681.211f);
+    case 240:
+        return static_cast<u32>(
+            ((static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+             (command.pitch * 0.000030518f)) *
+                2550.414f +
+            9663.969f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(
+    const PcmFloatDataSourceVersion2Command& command) const {
+    switch (sample_count) {
+    case 160:
+        switch (command.src_quality) {
+        case SrcQuality::Medium:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        1672.026f +
+                                    7681.211f);
+        case SrcQuality::High:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        1672.982f +
+                                    9038.011f);
+        case SrcQuality::Low:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        1673.216f +
+                                    6027.577f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid SRC quality {}",
+                      static_cast<u32>(command.src_quality));
+            return 0;
+        }
+
+    case 240:
+        switch (command.src_quality) {
+        case SrcQuality::Medium:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        2550.414f +
+                                    9663.969f);
+        case SrcQuality::High:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        2522.303f +
+                                    11758.571f);
+        case SrcQuality::Low:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        2537.061f +
+                                    7369.309f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid SRC quality {}",
+                      static_cast<u32>(command.src_quality));
+            return 0;
+        }
+
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(
+    const AdpcmDataSourceVersion1Command& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(
+            ((static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+             (command.pitch * 0.000030518f)) *
+                1827.665f +
+            7913.808f);
+    case 240:
+        return static_cast<u32>(
+            ((static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+             (command.pitch * 0.000030518f)) *
+                2756.372f +
+            9736.702f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(
+    const AdpcmDataSourceVersion2Command& command) const {
+    switch (sample_count) {
+    case 160:
+        switch (command.src_quality) {
+        case SrcQuality::Medium:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        1827.665f +
+                                    7913.808f);
+        case SrcQuality::High:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        1829.285f +
+                                    9607.814f);
+        case SrcQuality::Low:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        1824.609f +
+                                    6517.476f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid SRC quality {}",
+                      static_cast<u32>(command.src_quality));
+            return 0;
+        }
+
+    case 240:
+        switch (command.src_quality) {
+        case SrcQuality::Medium:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        2756.372f +
+                                    9736.702f);
+        case SrcQuality::High:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        2731.308f +
+                                    12154.379f);
+        case SrcQuality::Low:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        2732.152f +
+                                    7929.442f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid SRC quality {}",
+                      static_cast<u32>(command.src_quality));
+            return 0;
+        }
+
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(
+    [[maybe_unused]] const VolumeCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(1311.1f);
+    case 240:
+        return static_cast<u32>(1713.6f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(
+    [[maybe_unused]] const VolumeRampCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(1425.3f);
+    case 240:
+        return static_cast<u32>(1700.0f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(
+    [[maybe_unused]] const BiquadFilterCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(4173.2f);
+    case 240:
+        return static_cast<u32>(5585.1f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(
+    [[maybe_unused]] const MixCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(1402.8f);
+    case 240:
+        return static_cast<u32>(1853.2f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(
+    [[maybe_unused]] const MixRampCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(1968.7f);
+    case 240:
+        return static_cast<u32>(2459.4f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(const MixRampGroupedCommand& command) const {
+    u32 count{0};
+    for (u32 i = 0; i < command.buffer_count; i++) {
+        if (command.volumes[i] != 0.0f || command.prev_volumes[i] != 0.0f) {
+            count++;
+        }
+    }
+
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>((static_cast<f32>(sample_count) * 6.708f) *
+                                static_cast<f32>(count));
+    case 240:
+        return static_cast<u32>((static_cast<f32>(sample_count) * 6.443f) *
+                                static_cast<f32>(count));
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(
+    [[maybe_unused]] const DepopPrepareCommand& command) const {
+    return 0;
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(
+    [[maybe_unused]] const DepopForMixBuffersCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(739.64f);
+    case 240:
+        return static_cast<u32>(910.97f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(const DelayCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(8929.042f);
+            case 2:
+                return static_cast<u32>(25500.75f);
+            case 4:
+                return static_cast<u32>(47759.617f);
+            case 6:
+                return static_cast<u32>(82203.07f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(1295.206f);
+        case 2:
+            return static_cast<u32>(1213.6f);
+        case 4:
+            return static_cast<u32>(942.028f);
+        case 6:
+            return static_cast<u32>(1001.553f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    case 240:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(11941.051f);
+            case 2:
+                return static_cast<u32>(37197.371f);
+            case 4:
+                return static_cast<u32>(69749.836f);
+            case 6:
+                return static_cast<u32>(120042.398f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(997.668f);
+        case 2:
+            return static_cast<u32>(977.634f);
+        case 4:
+            return static_cast<u32>(792.309f);
+        case 6:
+            return static_cast<u32>(875.427f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(
+    [[maybe_unused]] const UpsampleCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(312990.0f);
+    case 240:
+        return static_cast<u32>(0.0f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(
+    [[maybe_unused]] const DownMix6chTo2chCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(9949.7f);
+    case 240:
+        return static_cast<u32>(14679.0f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(const AuxCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        if (command.enabled) {
+            return static_cast<u32>(7182.136f);
+        }
+        return static_cast<u32>(472.111f);
+    case 240:
+        if (command.enabled) {
+            return static_cast<u32>(9435.961f);
+        }
+        return static_cast<u32>(462.619f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(const DeviceSinkCommand& command) const {
+    switch (command.input_count) {
+    case 2:
+        switch (sample_count) {
+        case 160:
+            return static_cast<u32>(8979.956f);
+        case 240:
+            return static_cast<u32>(9221.907f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+            return 0;
+        }
+    case 6:
+        switch (sample_count) {
+        case 160:
+            return static_cast<u32>(9177.903f);
+        case 240:
+            return static_cast<u32>(9725.897f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+            return 0;
+        }
+    default:
+        LOG_ERROR(Service_Audio, "Invalid input count {}", command.input_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(
+    const CircularBufferSinkCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(static_cast<f32>(command.input_count) * 531.069f + 0.0f);
+    case 240:
+        return static_cast<u32>(static_cast<f32>(command.input_count) * 770.257f + 0.0f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(const ReverbCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(81475.055f);
+            case 2:
+                return static_cast<u32>(84975.0f);
+            case 4:
+                return static_cast<u32>(91625.148f);
+            case 6:
+                return static_cast<u32>(95332.266f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(536.298f);
+        case 2:
+            return static_cast<u32>(588.798f);
+        case 4:
+            return static_cast<u32>(643.702f);
+        case 6:
+            return static_cast<u32>(705.999f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    case 240:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(120174.469f);
+            case 2:
+                return static_cast<u32>(125262.219f);
+            case 4:
+                return static_cast<u32>(135751.234f);
+            case 6:
+                return static_cast<u32>(141129.234f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(617.641f);
+        case 2:
+            return static_cast<u32>(659.536f);
+        case 4:
+            return static_cast<u32>(711.438f);
+        case 6:
+            return static_cast<u32>(778.071f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(const I3dl2ReverbCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(116754.984f);
+            case 2:
+                return static_cast<u32>(125912.055f);
+            case 4:
+                return static_cast<u32>(146336.031f);
+            case 6:
+                return static_cast<u32>(165812.656f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(735.0f);
+        case 2:
+            return static_cast<u32>(766.615f);
+        case 4:
+            return static_cast<u32>(834.067f);
+        case 6:
+            return static_cast<u32>(875.437f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    case 240:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(170292.344f);
+            case 2:
+                return static_cast<u32>(183875.625f);
+            case 4:
+                return static_cast<u32>(214696.188f);
+            case 6:
+                return static_cast<u32>(243846.766f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(508.473f);
+        case 2:
+            return static_cast<u32>(582.445f);
+        case 4:
+            return static_cast<u32>(626.419f);
+        case 6:
+            return static_cast<u32>(682.468f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(
+    [[maybe_unused]] const PerformanceCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(498.17f);
+    case 240:
+        return static_cast<u32>(489.42f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(
+    [[maybe_unused]] const ClearMixBufferCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(static_cast<f32>(buffer_count - 1) * 266.645f + 0.0f);
+    case 240:
+        return static_cast<u32>(static_cast<f32>(buffer_count - 1) * 440.681f + 0.0f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(
+    [[maybe_unused]] const CopyMixBufferCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(842.59f);
+    case 240:
+        return static_cast<u32>(986.72f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(
+    const LightLimiterVersion1Command& command) const {
+    switch (sample_count) {
+    case 160:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(21392.383f);
+            case 2:
+                return static_cast<u32>(26829.389f);
+            case 4:
+                return static_cast<u32>(32405.152f);
+            case 6:
+                return static_cast<u32>(52218.586f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(897.004f);
+        case 2:
+            return static_cast<u32>(931.549f);
+        case 4:
+            return static_cast<u32>(975.387f);
+        case 6:
+            return static_cast<u32>(1016.778f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    case 240:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(30555.504f);
+            case 2:
+                return static_cast<u32>(39010.785f);
+            case 4:
+                return static_cast<u32>(48270.18f);
+            case 6:
+                return static_cast<u32>(76711.875f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(874.429f);
+        case 2:
+            return static_cast<u32>(921.553f);
+        case 4:
+            return static_cast<u32>(945.262f);
+        case 6:
+            return static_cast<u32>(992.26f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(
+    const LightLimiterVersion2Command& command) const {
+    switch (sample_count) {
+    case 160:
+        if (command.enabled) {
+            if (command.parameter.statistics_enabled) {
+                switch (command.parameter.channel_count) {
+                case 1:
+                    return static_cast<u32>(23308.928f);
+                case 2:
+                    return static_cast<u32>(29954.062f);
+                case 4:
+                    return static_cast<u32>(35807.477f);
+                case 6:
+                    return static_cast<u32>(58339.773f);
+                default:
+                    LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                              command.parameter.channel_count);
+                    return 0;
+                }
+            }
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(21392.383f);
+            case 2:
+                return static_cast<u32>(26829.389f);
+            case 4:
+                return static_cast<u32>(32405.152f);
+            case 6:
+                return static_cast<u32>(52218.586f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(897.004f);
+        case 2:
+            return static_cast<u32>(931.549f);
+        case 4:
+            return static_cast<u32>(975.387f);
+        case 6:
+            return static_cast<u32>(1016.778f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    case 240:
+        if (command.enabled) {
+            if (command.parameter.statistics_enabled) {
+                switch (command.parameter.channel_count) {
+                case 1:
+                    return static_cast<u32>(33526.121f);
+                case 2:
+                    return static_cast<u32>(43549.355f);
+                case 4:
+                    return static_cast<u32>(52190.281f);
+                case 6:
+                    return static_cast<u32>(85526.516f);
+                default:
+                    LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                              command.parameter.channel_count);
+                    return 0;
+                }
+            }
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(30555.504f);
+            case 2:
+                return static_cast<u32>(39010.785f);
+            case 4:
+                return static_cast<u32>(48270.18f);
+            case 6:
+                return static_cast<u32>(76711.875f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(874.429f);
+        case 2:
+            return static_cast<u32>(921.553f);
+        case 4:
+            return static_cast<u32>(945.262f);
+        case 6:
+            return static_cast<u32>(992.26f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(
+    [[maybe_unused]] const MultiTapBiquadFilterCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(7424.5f);
+    case 240:
+        return static_cast<u32>(9730.4f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(const CaptureCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        if (command.enabled) {
+            return static_cast<u32>(426.982f);
+        }
+        return static_cast<u32>(4261.005f);
+    case 240:
+        if (command.enabled) {
+            return static_cast<u32>(435.204f);
+        }
+        return static_cast<u32>(5858.265f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion4::Estimate(
+    [[maybe_unused]] const CompressorCommand& command) const {
+    return 0;
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(
+    const PcmInt16DataSourceVersion1Command& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(
+            ((static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+             (command.pitch * 0.000030518f)) *
+                427.52f +
+            6329.442f);
+    case 240:
+        return static_cast<u32>(
+            ((static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+             (command.pitch * 0.000030518f)) *
+                710.143f +
+            7853.286f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(
+    const PcmInt16DataSourceVersion2Command& command) const {
+    switch (sample_count) {
+    case 160:
+        switch (command.src_quality) {
+        case SrcQuality::Medium:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        427.52f +
+                                    6329.442f);
+        case SrcQuality::High:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        371.876f +
+                                    8049.415f);
+        case SrcQuality::Low:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        423.43f +
+                                    5062.659f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid SRC quality {}",
+                      static_cast<u32>(command.src_quality));
+            return 0;
+        }
+
+    case 240:
+        switch (command.src_quality) {
+        case SrcQuality::Medium:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        710.143f +
+                                    7853.286f);
+        case SrcQuality::High:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        610.487f +
+                                    10138.842f);
+        case SrcQuality::Low:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        676.722f +
+                                    5810.962f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid SRC quality {}",
+                      static_cast<u32>(command.src_quality));
+            return 0;
+        }
+
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(
+    const PcmFloatDataSourceVersion1Command& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(
+            ((static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+             (command.pitch * 0.000030518f)) *
+                1672.026f +
+            7681.211f);
+    case 240:
+        return static_cast<u32>(
+            ((static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+             (command.pitch * 0.000030518f)) *
+                2550.414f +
+            9663.969f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(
+    const PcmFloatDataSourceVersion2Command& command) const {
+    switch (sample_count) {
+    case 160:
+        switch (command.src_quality) {
+        case SrcQuality::Medium:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        1672.026f +
+                                    7681.211f);
+        case SrcQuality::High:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        1672.982f +
+                                    9038.011f);
+        case SrcQuality::Low:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        1673.216f +
+                                    6027.577f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid SRC quality {}",
+                      static_cast<u32>(command.src_quality));
+            return 0;
+        }
+
+    case 240:
+        switch (command.src_quality) {
+        case SrcQuality::Medium:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        2550.414f +
+                                    9663.969f);
+        case SrcQuality::High:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        2522.303f +
+                                    11758.571f);
+        case SrcQuality::Low:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        2537.061f +
+                                    7369.309f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid SRC quality {}",
+                      static_cast<u32>(command.src_quality));
+            return 0;
+        }
+
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(
+    const AdpcmDataSourceVersion1Command& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(
+            ((static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+             (command.pitch * 0.000030518f)) *
+                1827.665f +
+            7913.808f);
+    case 240:
+        return static_cast<u32>(
+            ((static_cast<f32>(command.sample_rate) / 200.0f / static_cast<f32>(sample_count)) *
+             (command.pitch * 0.000030518f)) *
+                2756.372f +
+            9736.702f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(
+    const AdpcmDataSourceVersion2Command& command) const {
+    switch (sample_count) {
+    case 160:
+        switch (command.src_quality) {
+        case SrcQuality::Medium:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        1827.665f +
+                                    7913.808f);
+        case SrcQuality::High:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        1829.285f +
+                                    9607.814f);
+        case SrcQuality::Low:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        1824.609f +
+                                    6517.476f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid SRC quality {}",
+                      static_cast<u32>(command.src_quality));
+            return 0;
+        }
+
+    case 240:
+        switch (command.src_quality) {
+        case SrcQuality::Medium:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        2756.372f +
+                                    9736.702f);
+        case SrcQuality::High:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        2731.308f +
+                                    12154.379f);
+        case SrcQuality::Low:
+            return static_cast<u32>((((static_cast<f32>(command.sample_rate) / 200.0f /
+                                       static_cast<f32>(sample_count)) *
+                                      (command.pitch * 0.000030518f)) -
+                                     1.0f) *
+                                        2732.152f +
+                                    7929.442f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid SRC quality {}",
+                      static_cast<u32>(command.src_quality));
+            return 0;
+        }
+
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(
+    [[maybe_unused]] const VolumeCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(1311.1f);
+    case 240:
+        return static_cast<u32>(1713.6f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(
+    [[maybe_unused]] const VolumeRampCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(1425.3f);
+    case 240:
+        return static_cast<u32>(1700.0f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(
+    [[maybe_unused]] const BiquadFilterCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(4173.2f);
+    case 240:
+        return static_cast<u32>(5585.1f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(
+    [[maybe_unused]] const MixCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(1402.8f);
+    case 240:
+        return static_cast<u32>(1853.2f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(
+    [[maybe_unused]] const MixRampCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(1968.7f);
+    case 240:
+        return static_cast<u32>(2459.4f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(const MixRampGroupedCommand& command) const {
+    u32 count{0};
+    for (u32 i = 0; i < command.buffer_count; i++) {
+        if (command.volumes[i] != 0.0f || command.prev_volumes[i] != 0.0f) {
+            count++;
+        }
+    }
+
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>((static_cast<f32>(sample_count) * 6.708f) *
+                                static_cast<f32>(count));
+    case 240:
+        return static_cast<u32>((static_cast<f32>(sample_count) * 6.443f) *
+                                static_cast<f32>(count));
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(
+    [[maybe_unused]] const DepopPrepareCommand& command) const {
+    return 0;
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(
+    [[maybe_unused]] const DepopForMixBuffersCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(739.64f);
+    case 240:
+        return static_cast<u32>(910.97f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(const DelayCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(8929.042f);
+            case 2:
+                return static_cast<u32>(25500.75f);
+            case 4:
+                return static_cast<u32>(47759.617f);
+            case 6:
+                return static_cast<u32>(82203.07f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(1295.206f);
+        case 2:
+            return static_cast<u32>(1213.6f);
+        case 4:
+            return static_cast<u32>(942.028f);
+        case 6:
+            return static_cast<u32>(1001.553f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    case 240:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(11941.051f);
+            case 2:
+                return static_cast<u32>(37197.371f);
+            case 4:
+                return static_cast<u32>(69749.836f);
+            case 6:
+                return static_cast<u32>(120042.398f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(997.668f);
+        case 2:
+            return static_cast<u32>(977.634f);
+        case 4:
+            return static_cast<u32>(792.309f);
+        case 6:
+            return static_cast<u32>(875.427f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(
+    [[maybe_unused]] const UpsampleCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(312990.0f);
+    case 240:
+        return static_cast<u32>(0.0f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(
+    [[maybe_unused]] const DownMix6chTo2chCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(9949.7f);
+    case 240:
+        return static_cast<u32>(14679.0f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(const AuxCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        if (command.enabled) {
+            return static_cast<u32>(7182.136f);
+        }
+        return static_cast<u32>(472.111f);
+    case 240:
+        if (command.enabled) {
+            return static_cast<u32>(9435.961f);
+        }
+        return static_cast<u32>(462.619f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(const DeviceSinkCommand& command) const {
+    switch (command.input_count) {
+    case 2:
+        switch (sample_count) {
+        case 160:
+            return static_cast<u32>(8979.956f);
+        case 240:
+            return static_cast<u32>(9221.907f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+            return 0;
+        }
+    case 6:
+        switch (sample_count) {
+        case 160:
+            return static_cast<u32>(9177.903f);
+        case 240:
+            return static_cast<u32>(9725.897f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+            return 0;
+        }
+    default:
+        LOG_ERROR(Service_Audio, "Invalid input count {}", command.input_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(
+    const CircularBufferSinkCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(static_cast<f32>(command.input_count) * 531.069f + 0.0f);
+    case 240:
+        return static_cast<u32>(static_cast<f32>(command.input_count) * 770.257f + 0.0f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(const ReverbCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(81475.055f);
+            case 2:
+                return static_cast<u32>(84975.0f);
+            case 4:
+                return static_cast<u32>(91625.148f);
+            case 6:
+                return static_cast<u32>(95332.266f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(536.298f);
+        case 2:
+            return static_cast<u32>(588.798f);
+        case 4:
+            return static_cast<u32>(643.702f);
+        case 6:
+            return static_cast<u32>(705.999f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    case 240:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(120174.469f);
+            case 2:
+                return static_cast<u32>(125262.219f);
+            case 4:
+                return static_cast<u32>(135751.234f);
+            case 6:
+                return static_cast<u32>(141129.234f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(617.641f);
+        case 2:
+            return static_cast<u32>(659.536f);
+        case 4:
+            return static_cast<u32>(711.438f);
+        case 6:
+            return static_cast<u32>(778.071f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(const I3dl2ReverbCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(116754.984f);
+            case 2:
+                return static_cast<u32>(125912.055f);
+            case 4:
+                return static_cast<u32>(146336.031f);
+            case 6:
+                return static_cast<u32>(165812.656f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(735.0f);
+        case 2:
+            return static_cast<u32>(766.615f);
+        case 4:
+            return static_cast<u32>(834.067f);
+        case 6:
+            return static_cast<u32>(875.437f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    case 240:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(170292.344f);
+            case 2:
+                return static_cast<u32>(183875.625f);
+            case 4:
+                return static_cast<u32>(214696.188f);
+            case 6:
+                return static_cast<u32>(243846.766f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(508.473f);
+        case 2:
+            return static_cast<u32>(582.445f);
+        case 4:
+            return static_cast<u32>(626.419f);
+        case 6:
+            return static_cast<u32>(682.468f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(
+    [[maybe_unused]] const PerformanceCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(498.17f);
+    case 240:
+        return static_cast<u32>(489.42f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(
+    [[maybe_unused]] const ClearMixBufferCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(static_cast<f32>(buffer_count - 1) * 266.645f + 0.0f);
+    case 240:
+        return static_cast<u32>(static_cast<f32>(buffer_count - 1) * 440.681f + 0.0f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(
+    [[maybe_unused]] const CopyMixBufferCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(842.59f);
+    case 240:
+        return static_cast<u32>(986.72f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(
+    const LightLimiterVersion1Command& command) const {
+    switch (sample_count) {
+    case 160:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(21508.01f);
+            case 2:
+                return static_cast<u32>(23120.453f);
+            case 4:
+                return static_cast<u32>(26270.053f);
+            case 6:
+                return static_cast<u32>(40471.902f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(897.004f);
+        case 2:
+            return static_cast<u32>(931.549f);
+        case 4:
+            return static_cast<u32>(975.387f);
+        case 6:
+            return static_cast<u32>(1016.778f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    case 240:
+        if (command.enabled) {
+            switch (command.parameter.channel_count) {
+            case 1:
+                return static_cast<u32>(30565.961f);
+            case 2:
+                return static_cast<u32>(32812.91f);
+            case 4:
+                return static_cast<u32>(37354.852f);
+            case 6:
+                return static_cast<u32>(58486.699f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                          command.parameter.channel_count);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(874.429f);
+        case 2:
+            return static_cast<u32>(921.553f);
+        case 4:
+            return static_cast<u32>(945.262f);
+        case 6:
+            return static_cast<u32>(992.26f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(
+    const LightLimiterVersion2Command& command) const {
+    switch (sample_count) {
+    case 160:
+        if (command.enabled) {
+            if (command.parameter.processing_mode == LightLimiterInfo::ProcessingMode::Mode0) {
+                if (command.parameter.statistics_enabled) {
+                    switch (command.parameter.channel_count) {
+                    case 1:
+                        return static_cast<u32>(23639.584f);
+                    case 2:
+                        return static_cast<u32>(24666.725f);
+                    case 4:
+                        return static_cast<u32>(28876.459f);
+                    case 6:
+                        return static_cast<u32>(47096.078f);
+                    default:
+                        LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                                  command.parameter.channel_count);
+                        return 0;
+                    }
+                } else {
+                    if (command.parameter.statistics_enabled) {
+                        switch (command.parameter.channel_count) {
+                        case 1:
+                            return static_cast<u32>(21508.01f);
+                        case 2:
+                            return static_cast<u32>(23120.453f);
+                        case 4:
+                            return static_cast<u32>(26270.053f);
+                        case 6:
+                            return static_cast<u32>(40471.902f);
+                        default:
+                            LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                                      command.parameter.channel_count);
+                            return 0;
+                        }
+                    }
+                }
+            } else if (command.parameter.processing_mode ==
+                       LightLimiterInfo::ProcessingMode::Mode1) {
+                if (command.parameter.statistics_enabled) {
+                    switch (command.parameter.channel_count) {
+                    case 1:
+                        return static_cast<u32>(23639.584f);
+                    case 2:
+                        return static_cast<u32>(29954.062f);
+                    case 4:
+                        return static_cast<u32>(35807.477f);
+                    case 6:
+                        return static_cast<u32>(58339.773f);
+                    default:
+                        LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                                  command.parameter.channel_count);
+                        return 0;
+                    }
+                } else {
+                    if (command.parameter.statistics_enabled) {
+                        switch (command.parameter.channel_count) {
+                        case 1:
+                            return static_cast<u32>(23639.584f);
+                        case 2:
+                            return static_cast<u32>(29954.062f);
+                        case 4:
+                            return static_cast<u32>(35807.477f);
+                        case 6:
+                            return static_cast<u32>(58339.773f);
+                        default:
+                            LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                                      command.parameter.channel_count);
+                            return 0;
+                        }
+                    }
+                }
+            } else {
+                LOG_ERROR(Service_Audio, "Invalid processing mode {}",
+                          command.parameter.processing_mode);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(897.004f);
+        case 2:
+            return static_cast<u32>(931.549f);
+        case 4:
+            return static_cast<u32>(975.387f);
+        case 6:
+            return static_cast<u32>(1016.778f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    case 240:
+        if (command.enabled) {
+            if (command.parameter.processing_mode == LightLimiterInfo::ProcessingMode::Mode0) {
+                if (command.parameter.statistics_enabled) {
+                    switch (command.parameter.channel_count) {
+                    case 1:
+                        return static_cast<u32>(33875.023f);
+                    case 2:
+                        return static_cast<u32>(35199.938f);
+                    case 4:
+                        return static_cast<u32>(41371.230f);
+                    case 6:
+                        return static_cast<u32>(68370.914f);
+                    default:
+                        LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                                  command.parameter.channel_count);
+                        return 0;
+                    }
+                } else {
+                    switch (command.parameter.channel_count) {
+                    case 1:
+                        return static_cast<u32>(30565.961f);
+                    case 2:
+                        return static_cast<u32>(32812.91f);
+                    case 4:
+                        return static_cast<u32>(37354.852f);
+                    case 6:
+                        return static_cast<u32>(58486.699f);
+                    default:
+                        LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                                  command.parameter.channel_count);
+                        return 0;
+                    }
+                }
+            } else if (command.parameter.processing_mode ==
+                       LightLimiterInfo::ProcessingMode::Mode1) {
+                if (command.parameter.statistics_enabled) {
+                    switch (command.parameter.channel_count) {
+                    case 1:
+                        return static_cast<u32>(33942.980f);
+                    case 2:
+                        return static_cast<u32>(28698.893f);
+                    case 4:
+                        return static_cast<u32>(34774.277f);
+                    case 6:
+                        return static_cast<u32>(61897.773f);
+                    default:
+                        LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                                  command.parameter.channel_count);
+                        return 0;
+                    }
+                } else {
+                    switch (command.parameter.channel_count) {
+                    case 1:
+                        return static_cast<u32>(30610.248f);
+                    case 2:
+                        return static_cast<u32>(26322.408f);
+                    case 4:
+                        return static_cast<u32>(30369.000f);
+                    case 6:
+                        return static_cast<u32>(51892.090f);
+                    default:
+                        LOG_ERROR(Service_Audio, "Invalid channel count {}",
+                                  command.parameter.channel_count);
+                        return 0;
+                    }
+                }
+            } else {
+                LOG_ERROR(Service_Audio, "Invalid processing mode {}",
+                          command.parameter.processing_mode);
+                return 0;
+            }
+        }
+        switch (command.parameter.channel_count) {
+        case 1:
+            return static_cast<u32>(874.429f);
+        case 2:
+            return static_cast<u32>(921.553f);
+        case 4:
+            return static_cast<u32>(945.262f);
+        case 6:
+            return static_cast<u32>(992.26f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(
+    [[maybe_unused]] const MultiTapBiquadFilterCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        return static_cast<u32>(7424.5f);
+    case 240:
+        return static_cast<u32>(9730.4f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(const CaptureCommand& command) const {
+    switch (sample_count) {
+    case 160:
+        if (command.enabled) {
+            return static_cast<u32>(426.982f);
+        }
+        return static_cast<u32>(4261.005f);
+    case 240:
+        if (command.enabled) {
+            return static_cast<u32>(435.204f);
+        }
+        return static_cast<u32>(5858.265f);
+    default:
+        LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+        return 0;
+    }
+}
+
+u32 CommandProcessingTimeEstimatorVersion5::Estimate(const CompressorCommand& command) const {
+    if (command.enabled) {
+        switch (command.parameter.channel_count) {
+        case 1:
+            switch (sample_count) {
+            case 160:
+                return static_cast<u32>(34430.570f);
+            case 240:
+                return static_cast<u32>(51095.348f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+                return 0;
+            }
+        case 2:
+            switch (sample_count) {
+            case 160:
+                return static_cast<u32>(44253.320f);
+            case 240:
+                return static_cast<u32>(65693.094f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+                return 0;
+            }
+        case 4:
+            switch (sample_count) {
+            case 160:
+                return static_cast<u32>(63827.457f);
+            case 240:
+                return static_cast<u32>(95382.852f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+                return 0;
+            }
+        case 6:
+            switch (sample_count) {
+            case 160:
+                return static_cast<u32>(83361.484f);
+            case 240:
+                return static_cast<u32>(124509.906f);
+            default:
+                LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+                return 0;
+            }
+        default:
+            LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+            return 0;
+        }
+    }
+    switch (command.parameter.channel_count) {
+    case 1:
+        switch (sample_count) {
+        case 160:
+            return static_cast<u32>(630.115f);
+        case 240:
+            return static_cast<u32>(840.136f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+            return 0;
+        }
+    case 2:
+        switch (sample_count) {
+        case 160:
+            return static_cast<u32>(638.274f);
+        case 240:
+            return static_cast<u32>(826.098f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+            return 0;
+        }
+    case 4:
+        switch (sample_count) {
+        case 160:
+            return static_cast<u32>(705.862f);
+        case 240:
+            return static_cast<u32>(901.876f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+            return 0;
+        }
+    case 6:
+        switch (sample_count) {
+        case 160:
+            return static_cast<u32>(782.019f);
+        case 240:
+            return static_cast<u32>(965.286f);
+        default:
+            LOG_ERROR(Service_Audio, "Invalid sample count {}", sample_count);
+            return 0;
+        }
+    default:
+        LOG_ERROR(Service_Audio, "Invalid channel count {}", command.parameter.channel_count);
+        return 0;
+    }
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/command_processing_time_estimator.h b/src/audio_core/renderer/command/command_processing_time_estimator.h
new file mode 100644
index 0000000000..4522171967
--- /dev/null
+++ b/src/audio_core/renderer/command/command_processing_time_estimator.h
@@ -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::AudioRenderer {
+/**
+ * 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::AudioRenderer
diff --git a/src/audio_core/renderer/command/commands.h b/src/audio_core/renderer/command/commands.h
new file mode 100644
index 0000000000..6d8b8546da
--- /dev/null
+++ b/src/audio_core/renderer/command/commands.h
@@ -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"
diff --git a/src/audio_core/renderer/command/data_source/adpcm.cpp b/src/audio_core/renderer/command/data_source/adpcm.cpp
new file mode 100644
index 0000000000..e66ed29909
--- /dev/null
+++ b/src/audio_core/renderer/command/data_source/adpcm.cpp
@@ -0,0 +1,84 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <span>
+
+#include "audio_core/renderer/adsp/command_list_processor.h"
+#include "audio_core/renderer/command/data_source/adpcm.h"
+#include "audio_core/renderer/command/data_source/decode.h"
+
+namespace AudioCore::AudioRenderer {
+
+void AdpcmDataSourceVersion1Command::Dump(const ADSP::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 ADSP::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 AdpcmDataSourceVersion1Command::Verify(const ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+void AdpcmDataSourceVersion2Command::Dump(const ADSP::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 ADSP::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 ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/data_source/adpcm.h b/src/audio_core/renderer/command/data_source/adpcm.h
new file mode 100644
index 0000000000..a9cf9cee45
--- /dev/null
+++ b/src/audio_core/renderer/command/data_source/adpcm.h
@@ -0,0 +1,119 @@
+// 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::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::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::AudioRenderer::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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::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::AudioRenderer::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::AudioRenderer
diff --git a/src/audio_core/renderer/command/data_source/decode.cpp b/src/audio_core/renderer/command/data_source/decode.cpp
new file mode 100644
index 0000000000..ff5d31bd6f
--- /dev/null
+++ b/src/audio_core/renderer/command/data_source/decode.cpp
@@ -0,0 +1,428 @@
+// 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 "core/memory.h"
+
+namespace AudioCore::AudioRenderer {
+
+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};
+        const u64 size_bytes{size * sizeof(T)};
+
+        std::vector<T> samples(size);
+        memory.ReadBlockUnsafe(source, samples.data(), size_bytes);
+
+        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))};
+        std::vector<T> samples(samples_to_decode);
+        memory.ReadBlockUnsafe(source, samples.data(), samples_to_decode * sizeof(T));
+
+        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 samples_to_process{
+        std::min(req.end_offset - req.start_offset - req.offset, req.samples_to_read)};
+
+    auto samples_to_read{samples_to_process};
+    auto start_pos{req.start_offset + req.offset};
+    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)};
+    std::vector<u8> wavebuffer(size);
+    memory.ReadBlockUnsafe(req.buffer + position_in_frame / 2, wavebuffer.data(),
+                           wavebuffer.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) {
+    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>(args.source_sample_rate) / args.target_sample_rate) *
+        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::vector<s16> temp_buffer(TempBufferSize, 0);
+
+    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 != 0 && wavebuffer.loop_end_offset != 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 == 0 || offset >= end_offset - start_offset) {
+                offset = 0;
+                if (!wavebuffer.loop) {
+                    voice_state.wave_buffer_valid[wavebuffer_index] = false;
+                    voice_state.loop_count = 0;
+
+                    if (wavebuffer.stream_ended) {
+                        played_sample_count = 0;
+                    }
+
+                    wavebuffer_index = (wavebuffer_index + 1) % MaxWaveBuffers;
+                    wavebuffers_consumed++;
+                } else {
+                    voice_state.loop_count++;
+                    if (wavebuffer.loop_count > 0 &&
+                        (voice_state.loop_count > wavebuffer.loop_count || samples_decoded == 0)) {
+                        voice_state.wave_buffer_valid[wavebuffer_index] = false;
+                        voice_state.loop_count = 0;
+
+                        if (wavebuffer.stream_ended) {
+                            played_sample_count = 0;
+                        }
+
+                        wavebuffer_index = (wavebuffer_index + 1) % MaxWaveBuffers;
+                        wavebuffers_consumed++;
+                    }
+
+                    if (samples_decoded == 0) {
+                        is_buffer_starved = true;
+                        break;
+                    }
+
+                    if (args.IsVoicePlayedSampleCountResetAtLoopPointSupported) {
+                        played_sample_count = 0;
+                    }
+                }
+            }
+        }
+
+        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::AudioRenderer
diff --git a/src/audio_core/renderer/command/data_source/decode.h b/src/audio_core/renderer/command/data_source/decode.h
new file mode 100644
index 0000000000..4d63d6fa8a
--- /dev/null
+++ b/src/audio_core/renderer/command/data_source/decode.h
@@ -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::AudioRenderer {
+
+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::AudioRenderer
diff --git a/src/audio_core/renderer/command/data_source/pcm_float.cpp b/src/audio_core/renderer/command/data_source/pcm_float.cpp
new file mode 100644
index 0000000000..be77fab694
--- /dev/null
+++ b/src/audio_core/renderer/command/data_source/pcm_float.cpp
@@ -0,0 +1,86 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/adsp/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::AudioRenderer {
+
+void PcmFloatDataSourceVersion1Command::Dump(const ADSP::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 ADSP::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 PcmFloatDataSourceVersion1Command::Verify(const ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+void PcmFloatDataSourceVersion2Command::Dump(const ADSP::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 ADSP::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 ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/data_source/pcm_float.h b/src/audio_core/renderer/command/data_source/pcm_float.h
new file mode 100644
index 0000000000..e4af77c200
--- /dev/null
+++ b/src/audio_core/renderer/command/data_source/pcm_float.h
@@ -0,0 +1,113 @@
+// 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::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::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::AudioRenderer::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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::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::AudioRenderer::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::AudioRenderer
diff --git a/src/audio_core/renderer/command/data_source/pcm_int16.cpp b/src/audio_core/renderer/command/data_source/pcm_int16.cpp
new file mode 100644
index 0000000000..7a27463e4f
--- /dev/null
+++ b/src/audio_core/renderer/command/data_source/pcm_int16.cpp
@@ -0,0 +1,87 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <span>
+
+#include "audio_core/renderer/adsp/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::AudioRenderer {
+
+void PcmInt16DataSourceVersion1Command::Dump(const ADSP::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 ADSP::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 PcmInt16DataSourceVersion1Command::Verify(const ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+void PcmInt16DataSourceVersion2Command::Dump(const ADSP::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 ADSP::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 ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/data_source/pcm_int16.h b/src/audio_core/renderer/command/data_source/pcm_int16.h
new file mode 100644
index 0000000000..5de1ad60d5
--- /dev/null
+++ b/src/audio_core/renderer/command/data_source/pcm_int16.h
@@ -0,0 +1,110 @@
+// 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::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::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::AudioRenderer::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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::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::AudioRenderer::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::AudioRenderer
diff --git a/src/audio_core/renderer/command/effect/aux_.cpp b/src/audio_core/renderer/command/effect/aux_.cpp
new file mode 100644
index 0000000000..e76db893fb
--- /dev/null
+++ b/src/audio_core/renderer/command/effect/aux_.cpp
@@ -0,0 +1,207 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/adsp/command_list_processor.h"
+#include "audio_core/renderer/command/effect/aux_.h"
+#include "audio_core/renderer/effect/aux_.h"
+#include "core/memory.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * 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;
+    }
+
+    auto info{reinterpret_cast<AuxInfo::AuxInfoDsp*>(memory.GetPointer(aux_info))};
+    info->read_offset = 0;
+    info->write_offset = 0;
+    info->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_   - 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, const CpuAddr send_info_,
+                             [[maybe_unused]] u32 sample_count, const CpuAddr send_buffer,
+                             const 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 (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 || 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) {
+        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 writing.
+ * @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, const CpuAddr return_info_,
+                            const CpuAddr return_buffer, const u32 count_max, std::span<s32> output,
+                            const u32 count_, const u32 read_offset, const u32 update_count) {
+    if (count_max == 0) {
+        return 0;
+    }
+
+    if (count_ > count_max) {
+        LOG_ERROR(Service_Audio, "count must be smaller than count_max! count {}, count_max {}",
+                  count_, count_max);
+        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{count_};
+    u32 read_pos{0};
+    while (read_count > 0) {
+        u32 to_read{std::min(count_max - target_read_offset, read_count)};
+
+        if (to_read > 0) {
+            memory.ReadBlockUnsafe(return_buffer + target_read_offset * sizeof(s32),
+                                   &output[read_pos], to_read * sizeof(s32));
+        }
+
+        target_read_offset = (target_read_offset + to_read) % count_max;
+        read_count -= to_read;
+        read_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 count_;
+}
+
+void AuxCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor,
+                      std::string& string) {
+    string += fmt::format("AuxCommand\n\tenabled {} input {:02X} output {:02X}\n", effect_enabled,
+                          input, output);
+}
+
+void AuxCommand::Process(const ADSP::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);
+        }
+    } 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 ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/effect/aux_.h b/src/audio_core/renderer/command/effect/aux_.h
new file mode 100644
index 0000000000..825c93732b
--- /dev/null
+++ b/src/audio_core/renderer/command/effect/aux_.h
@@ -0,0 +1,66 @@
+// 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::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::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::AudioRenderer
diff --git a/src/audio_core/renderer/command/effect/biquad_filter.cpp b/src/audio_core/renderer/command/effect/biquad_filter.cpp
new file mode 100644
index 0000000000..1baae74fd6
--- /dev/null
+++ b/src/audio_core/renderer/command/effect/biquad_filter.cpp
@@ -0,0 +1,118 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/adsp/command_list_processor.h"
+#include "audio_core/renderer/command/effect/biquad_filter.h"
+#include "audio_core/renderer/voice/voice_state.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * 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 s64 min{std::numeric_limits<s32>::min()};
+    constexpr s64 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{state.s0.to_double(), state.s1.to_double(), state.s2.to_double(),
+                         state.s3.to_double()};
+
+    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(static_cast<s64>(sample), min, max));
+
+        s[1] = s[0];
+        s[0] = in_sample;
+        s[3] = s[2];
+        s[2] = sample;
+    }
+
+    state.s0 = s[0];
+    state.s1 = s[1];
+    state.s2 = s[2];
+    state.s3 = 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()};
+    std::array<Common::FixedPoint<50, 14>, 3> b{
+        Common::FixedPoint<50, 14>::from_base(b_[0]),
+        Common::FixedPoint<50, 14>::from_base(b_[1]),
+        Common::FixedPoint<50, 14>::from_base(b_[2]),
+    };
+    std::array<Common::FixedPoint<50, 14>, 3> a{
+        Common::FixedPoint<50, 14>::from_base(a_[0]),
+        Common::FixedPoint<50, 14>::from_base(a_[1]),
+    };
+
+    for (u32 i = 0; i < sample_count; i++) {
+        s64 in_sample{input[i]};
+        auto sample{in_sample * b[0] + state.s0};
+        const auto out_sample{std::clamp(sample.to_long(), min, max)};
+
+        output[i] = static_cast<s32>(out_sample);
+
+        state.s0 = state.s1 + b[1] * in_sample + a[0] * out_sample;
+        state.s1 = 0 + b[2] * in_sample + a[1] * out_sample;
+    }
+}
+
+void BiquadFilterCommand::Dump([[maybe_unused]] const ADSP::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 ADSP::CommandListProcessor& processor) {
+    auto state_{reinterpret_cast<VoiceState::BiquadFilterState*>(state)};
+    if (needs_init) {
+        std::memset(state_, 0, sizeof(VoiceState::BiquadFilterState));
+    }
+
+    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 ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/effect/biquad_filter.h b/src/audio_core/renderer/command/effect/biquad_filter.h
new file mode 100644
index 0000000000..4c9c42d29a
--- /dev/null
+++ b/src/audio_core/renderer/command/effect/biquad_filter.h
@@ -0,0 +1,74 @@
+// 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::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::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::AudioRenderer
diff --git a/src/audio_core/renderer/command/effect/capture.cpp b/src/audio_core/renderer/command/effect/capture.cpp
new file mode 100644
index 0000000000..042fd286e4
--- /dev/null
+++ b/src/audio_core/renderer/command/effect/capture.cpp
@@ -0,0 +1,142 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/adsp/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::AudioRenderer {
+/**
+ * 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 ADSP::CommandListProcessor& processor,
+                          std::string& string) {
+    string += fmt::format("CaptureCommand\n\tenabled {} input {:02X} output {:02X}", effect_enabled,
+                          input, output);
+}
+
+void CaptureCommand::Process(const ADSP::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 ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/effect/capture.h b/src/audio_core/renderer/command/effect/capture.h
new file mode 100644
index 0000000000..8670acb24e
--- /dev/null
+++ b/src/audio_core/renderer/command/effect/capture.h
@@ -0,0 +1,62 @@
+// 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::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::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::AudioRenderer
diff --git a/src/audio_core/renderer/command/effect/compressor.cpp b/src/audio_core/renderer/command/effect/compressor.cpp
new file mode 100644
index 0000000000..2ebc140f13
--- /dev/null
+++ b/src/audio_core/renderer/command/effect/compressor.cpp
@@ -0,0 +1,156 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <cmath>
+#include <span>
+#include <vector>
+
+#include "audio_core/renderer/adsp/command_list_processor.h"
+#include "audio_core/renderer/command/effect/compressor.h"
+#include "audio_core/renderer/effect/compressor.h"
+
+namespace AudioCore::AudioRenderer {
+
+static void SetCompressorEffectParameter(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(CompressorInfo::ParameterVersion2& params,
+                                       CompressorInfo::State& state) {
+    std::memset(&state, 0, sizeof(CompressorInfo::State));
+
+    state.unk_00 = 0;
+    state.unk_04 = 1.0f;
+    state.unk_08 = 1.0f;
+
+    SetCompressorEffectParameter(params, state);
+}
+
+static void ApplyCompressorEffect(CompressorInfo::ParameterVersion2& params,
+                                  CompressorInfo::State& state, bool enabled,
+                                  std::vector<std::span<const s32>> input_buffers,
+                                  std::vector<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((char*)output_buffers[channel].data(),
+                            (char*)input_buffers[channel].data(),
+                            output_buffers[channel].size_bytes());
+            }
+        }
+    }
+}
+
+void CompressorCommand::Dump([[maybe_unused]] const ADSP::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 ADSP::CommandListProcessor& processor) {
+    std::vector<std::span<const s32>> input_buffers(parameter.channel_count);
+    std::vector<std::span<s32>> output_buffers(parameter.channel_count);
+
+    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 ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/effect/compressor.h b/src/audio_core/renderer/command/effect/compressor.h
new file mode 100644
index 0000000000..f8e96cb43f
--- /dev/null
+++ b/src/audio_core/renderer/command/effect/compressor.h
@@ -0,0 +1,60 @@
+// 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::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::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::AudioRenderer
diff --git a/src/audio_core/renderer/command/effect/delay.cpp b/src/audio_core/renderer/command/effect/delay.cpp
new file mode 100644
index 0000000000..a4e408d403
--- /dev/null
+++ b/src/audio_core/renderer/command/effect/delay.cpp
@@ -0,0 +1,238 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/adsp/command_list_processor.h"
+#include "audio_core/renderer/command/effect/delay.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * 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].buffer.size() == 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::vector<std::span<const s32>>& inputs,
+                       std::vector<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::vector<std::span<const s32>>& inputs,
+                             std::vector<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 ADSP::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 ADSP::CommandListProcessor& processor) {
+    std::vector<std::span<const s32>> input_buffers(parameter.channel_count);
+    std::vector<std::span<s32>> output_buffers(parameter.channel_count);
+
+    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 ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/effect/delay.h b/src/audio_core/renderer/command/effect/delay.h
new file mode 100644
index 0000000000..b7a15ae6b1
--- /dev/null
+++ b/src/audio_core/renderer/command/effect/delay.h
@@ -0,0 +1,60 @@
+// 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::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::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::AudioRenderer
diff --git a/src/audio_core/renderer/command/effect/i3dl2_reverb.cpp b/src/audio_core/renderer/command/effect/i3dl2_reverb.cpp
new file mode 100644
index 0000000000..c4bf3943a1
--- /dev/null
+++ b/src/audio_core/renderer/command/effect/i3dl2_reverb.cpp
@@ -0,0 +1,437 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <numbers>
+
+#include "audio_core/renderer/adsp/command_list_processor.h"
+#include "audio_core/renderer/command/effect/i3dl2_reverb.h"
+
+namespace AudioCore::AudioRenderer {
+
+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) {
+    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,
+    };
+    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,
+    };
+    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,
+    };
+    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 ADSP::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 ADSP::CommandListProcessor& processor) {
+    std::vector<std::span<const s32>> input_buffers(parameter.channel_count);
+    std::vector<std::span<s32>> output_buffers(parameter.channel_count);
+
+    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 ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/effect/i3dl2_reverb.h b/src/audio_core/renderer/command/effect/i3dl2_reverb.h
new file mode 100644
index 0000000000..243877056d
--- /dev/null
+++ b/src/audio_core/renderer/command/effect/i3dl2_reverb.h
@@ -0,0 +1,60 @@
+// 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::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::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::AudioRenderer
diff --git a/src/audio_core/renderer/command/effect/light_limiter.cpp b/src/audio_core/renderer/command/effect/light_limiter.cpp
new file mode 100644
index 0000000000..e8fb0e2fc7
--- /dev/null
+++ b/src/audio_core/renderer/command/effect/light_limiter.cpp
@@ -0,0 +1,222 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/adsp/command_list_processor.h"
+#include "audio_core/renderer/command/effect/light_limiter.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * 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::vector<std::span<const s32>>& inputs,
+                                    std::vector<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 ADSP::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 ADSP::CommandListProcessor& processor) {
+    std::vector<std::span<const s32>> input_buffers(parameter.channel_count);
+    std::vector<std::span<s32>> output_buffers(parameter.channel_count);
+
+    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 ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+void LightLimiterVersion2Command::Dump([[maybe_unused]] const ADSP::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 ADSP::CommandListProcessor& processor) {
+    std::vector<std::span<const s32>> input_buffers(parameter.channel_count);
+    std::vector<std::span<s32>> output_buffers(parameter.channel_count);
+
+    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 ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/effect/light_limiter.h b/src/audio_core/renderer/command/effect/light_limiter.h
new file mode 100644
index 0000000000..5d98272c79
--- /dev/null
+++ b/src/audio_core/renderer/command/effect/light_limiter.h
@@ -0,0 +1,103 @@
+// 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::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::CommandListProcessor& processor) override;
+
+    /**
+     * Verify this command's data is valid.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    bool Verify(const ADSP::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::AudioRenderer
diff --git a/src/audio_core/renderer/command/effect/multi_tap_biquad_filter.cpp b/src/audio_core/renderer/command/effect/multi_tap_biquad_filter.cpp
new file mode 100644
index 0000000000..b3c3ba4ba5
--- /dev/null
+++ b/src/audio_core/renderer/command/effect/multi_tap_biquad_filter.cpp
@@ -0,0 +1,45 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/adsp/command_list_processor.h"
+#include "audio_core/renderer/command/effect/biquad_filter.h"
+#include "audio_core/renderer/command/effect/multi_tap_biquad_filter.h"
+
+namespace AudioCore::AudioRenderer {
+
+void MultiTapBiquadFilterCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor,
+                                       std::string& string) {
+    string += fmt::format(
+        "MultiTapBiquadFilterCommand\n\tinput {:02X}\n\toutput {:02X}\n\tneeds_init ({}, {})\n",
+        input, output, needs_init[0], needs_init[1]);
+}
+
+void MultiTapBiquadFilterCommand::Process(const ADSP::CommandListProcessor& processor) {
+    if (filter_tap_count > MaxBiquadFilters) {
+        LOG_ERROR(Service_Audio, "Too many filter taps! {}", filter_tap_count);
+        filter_tap_count = MaxBiquadFilters;
+    }
+
+    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)};
+
+    // TODO: Fix this, currently just applies the filter to the input twice,
+    // and doesn't chain the biquads together at all.
+    for (u32 i = 0; i < filter_tap_count; i++) {
+        auto state{reinterpret_cast<VoiceState::BiquadFilterState*>(states[i])};
+        if (needs_init[i]) {
+            std::memset(state, 0, sizeof(VoiceState::BiquadFilterState));
+        }
+
+        ApplyBiquadFilterFloat(output_buffer, input_buffer, biquads[i].b, biquads[i].a, *state,
+                               processor.sample_count);
+    }
+}
+
+bool MultiTapBiquadFilterCommand::Verify(const ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/effect/multi_tap_biquad_filter.h b/src/audio_core/renderer/command/effect/multi_tap_biquad_filter.h
new file mode 100644
index 0000000000..99c2c08301
--- /dev/null
+++ b/src/audio_core/renderer/command/effect/multi_tap_biquad_filter.h
@@ -0,0 +1,59 @@
+// 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/voice/voice_info.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * AudioRenderer command for applying multiple biquads at once.
+ */
+struct MultiTapBiquadFilterCommand : 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::CommandListProcessor& processor) override;
+
+    /// Input mix buffer index
+    s16 input;
+    /// Output mix buffer index
+    s16 output;
+    /// Biquad parameters
+    std::array<VoiceInfo::BiquadFilterParameter, MaxBiquadFilters> biquads;
+    /// Biquad states, updated each call
+    std::array<CpuAddr, MaxBiquadFilters> states;
+    /// If each biquad needs initialisation
+    std::array<bool, MaxBiquadFilters> needs_init;
+    /// Number of active biquads
+    u8 filter_tap_count;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/effect/reverb.cpp b/src/audio_core/renderer/command/effect/reverb.cpp
new file mode 100644
index 0000000000..fe2b1eb431
--- /dev/null
+++ b/src/audio_core/renderer/command/effect/reverb.cpp
@@ -0,0 +1,440 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <numbers>
+#include <ranges>
+
+#include "audio_core/renderer/adsp/command_list_processor.h"
+#include "audio_core/renderer/command/effect/reverb.h"
+
+namespace AudioCore::AudioRenderer {
+
+constexpr std::array<f32, ReverbInfo::MaxDelayLines> FdnMaxDelayLineTimes = {
+    53.9532470703125f,
+    79.19256591796875f,
+    116.23876953125f,
+    170.61529541015625f,
+};
+
+constexpr std::array<f32, ReverbInfo::MaxDelayLines> DecayMaxDelayLineTimes = {
+    7.0f,
+    9.0f,
+    13.0f,
+    17.0f,
+};
+
+constexpr std::array<std::array<f32, ReverbInfo::MaxDelayTaps + 1>, ReverbInfo::NumEarlyModes>
+    EarlyDelayTimes = {
+        {{0.000000f, 3.500000f, 2.799988f, 3.899963f, 2.699951f, 13.399963f, 7.899963f, 8.399963f,
+          9.899963f, 12.000000f, 12.500000f},
+         {0.000000f, 11.799988f, 5.500000f, 11.199951f, 10.399963f, 38.099976f, 22.199951f,
+          29.599976f, 21.199951f, 24.799988f, 40.000000f},
+         {0.000000f, 41.500000f, 20.500000f, 41.299988f, 0.000000f, 29.500000f, 33.799988f,
+          45.199951f, 46.799988f, 0.000000f, 50.000000f},
+         {33.099976f, 43.299988f, 22.799988f, 37.899963f, 14.899963f, 35.299988f, 17.899963f,
+          34.199951f, 0.000000f, 43.299988f, 50.000000f},
+         {0.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f,
+          0.000000f, 0.000000f, 0.000000f}},
+};
+
+constexpr std::array<std::array<f32, ReverbInfo::MaxDelayTaps>, ReverbInfo::NumEarlyModes>
+    EarlyDelayGains = {{
+        {0.699951f, 0.679993f, 0.699951f, 0.679993f, 0.699951f, 0.679993f, 0.699951f, 0.679993f,
+         0.679993f, 0.679993f},
+        {0.699951f, 0.679993f, 0.699951f, 0.679993f, 0.699951f, 0.679993f, 0.679993f, 0.679993f,
+         0.679993f, 0.679993f},
+        {0.500000f, 0.699951f, 0.699951f, 0.679993f, 0.500000f, 0.679993f, 0.679993f, 0.699951f,
+         0.679993f, 0.000000f},
+        {0.929993f, 0.919983f, 0.869995f, 0.859985f, 0.939941f, 0.809998f, 0.799988f, 0.769958f,
+         0.759949f, 0.649963f},
+        {0.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f, 0.000000f,
+         0.000000f, 0.000000f},
+    }};
+
+constexpr std::array<std::array<f32, ReverbInfo::MaxDelayLines>, ReverbInfo::NumLateModes>
+    FdnDelayTimes = {{
+        {53.953247f, 79.192566f, 116.238770f, 130.615295f},
+        {53.953247f, 79.192566f, 116.238770f, 170.615295f},
+        {5.000000f, 10.000000f, 5.000000f, 10.000000f},
+        {47.029968f, 71.000000f, 103.000000f, 170.000000f},
+        {53.953247f, 79.192566f, 116.238770f, 170.615295f},
+    }};
+
+constexpr std::array<std::array<f32, ReverbInfo::MaxDelayLines>, ReverbInfo::NumLateModes>
+    DecayDelayTimes = {{
+        {7.000000f, 9.000000f, 13.000000f, 17.000000f},
+        {7.000000f, 9.000000f, 13.000000f, 17.000000f},
+        {1.000000f, 1.000000f, 1.000000f, 1.000000f},
+        {7.000000f, 7.000000f, 13.000000f, 9.000000f},
+        {7.000000f, 9.000000f, 13.000000f, 17.000000f},
+    }};
+
+/**
+ * Update the ReverbInfo state according to the given parameters.
+ *
+ * @param params - Input parameters to update the state.
+ * @param state  - State to be updated.
+ */
+static void UpdateReverbEffectParameter(const ReverbInfo::ParameterVersion2& params,
+                                        ReverbInfo::State& state) {
+    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 cos = [](f32 degrees) -> f32 {
+        return std::cos(degrees * std::numbers::pi_v<f32> / 180.0f);
+    };
+
+    static bool unk_initialized{false};
+    static Common::FixedPoint<50, 14> unk_value{};
+
+    const auto sample_rate{Common::FixedPoint<50, 14>::from_base(params.sample_rate)};
+    const auto pre_delay_time{Common::FixedPoint<50, 14>::from_base(params.pre_delay)};
+
+    for (u32 i = 0; i < ReverbInfo::MaxDelayTaps; i++) {
+        auto early_delay{
+            ((pre_delay_time + EarlyDelayTimes[params.early_mode][i]) * sample_rate).to_int()};
+        early_delay = std::min(early_delay, state.pre_delay_line.sample_count_max);
+        state.early_delay_times[i] = early_delay + 1;
+        state.early_gains[i] = Common::FixedPoint<50, 14>::from_base(params.early_gain) *
+                               EarlyDelayGains[params.early_mode][i];
+    }
+
+    if (params.channel_count == 2) {
+        state.early_gains[4] * 0.5f;
+        state.early_gains[5] * 0.5f;
+    }
+
+    auto pre_time{
+        ((pre_delay_time + EarlyDelayTimes[params.early_mode][10]) * sample_rate).to_int()};
+    state.pre_delay_time = std::min(pre_time, state.pre_delay_line.sample_count_max);
+
+    if (!unk_initialized) {
+        unk_value = cos((1280.0f / sample_rate).to_float());
+        unk_initialized = true;
+    }
+
+    for (u32 i = 0; i < ReverbInfo::MaxDelayLines; i++) {
+        const auto fdn_delay{(FdnDelayTimes[params.late_mode][i] * sample_rate).to_int()};
+        state.fdn_delay_lines[i].sample_count =
+            std::min(fdn_delay, state.fdn_delay_lines[i].sample_count_max);
+        state.fdn_delay_lines[i].buffer_end =
+            &state.fdn_delay_lines[i].buffer[state.fdn_delay_lines[i].sample_count - 1];
+
+        const auto decay_delay{(DecayDelayTimes[params.late_mode][i] * sample_rate).to_int()};
+        state.decay_delay_lines[i].sample_count =
+            std::min(decay_delay, state.decay_delay_lines[i].sample_count_max);
+        state.decay_delay_lines[i].buffer_end =
+            &state.decay_delay_lines[i].buffer[state.decay_delay_lines[i].sample_count - 1];
+
+        state.decay_delay_lines[i].decay =
+            0.5999755859375f * (1.0f - Common::FixedPoint<50, 14>::from_base(params.colouration));
+
+        auto a{(Common::FixedPoint<50, 14>(state.fdn_delay_lines[i].sample_count_max) +
+                state.decay_delay_lines[i].sample_count_max) *
+               -3};
+        auto b{a / (Common::FixedPoint<50, 14>::from_base(params.decay_time) * sample_rate)};
+        Common::FixedPoint<50, 14> c{0.0f};
+        Common::FixedPoint<50, 14> d{0.0f};
+        auto hf_decay_ratio{Common::FixedPoint<50, 14>::from_base(params.high_freq_decay_ratio)};
+
+        if (hf_decay_ratio > 0.99493408203125f) {
+            c = 0.0f;
+            d = 1.0f;
+        } else {
+            const auto e{
+                pow_10(((((1.0f / hf_decay_ratio) - 1.0f) * 2) / 100 * (b / 10)).to_float())};
+            const auto f{1.0f - e};
+            const auto g{2.0f - (unk_value * e * 2)};
+            const auto h{std::sqrt(std::pow(g.to_float(), 2.0f) - (std::pow(f, 2.0f) * 4))};
+
+            c = (g - h) / (f * 2.0f);
+            d = 1.0f - c;
+        }
+
+        state.hf_decay_prev_gain[i] = c;
+        state.hf_decay_gain[i] = pow_10((b / 1000).to_float()) * d * 0.70709228515625f;
+        state.prev_feedback_output[i] = 0;
+    }
+}
+
+/**
+ * Initialize a new ReverbInfo 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)
+ * @param long_size_pre_delay_supported - Use a longer pre-delay time before reverb begins.
+ */
+static void InitializeReverbEffect(const ReverbInfo::ParameterVersion2& params,
+                                   ReverbInfo::State& state, const CpuAddr workbuffer,
+                                   const bool long_size_pre_delay_supported) {
+    state = {};
+
+    auto delay{Common::FixedPoint<50, 14>::from_base(params.sample_rate)};
+
+    for (u32 i = 0; i < ReverbInfo::MaxDelayLines; i++) {
+        auto fdn_delay_time{(FdnMaxDelayLineTimes[i] * delay).to_uint_floor()};
+        state.fdn_delay_lines[i].Initialize(fdn_delay_time, 1.0f);
+
+        auto decay_delay_time{(DecayMaxDelayLineTimes[i] * delay).to_uint_floor()};
+        state.decay_delay_lines[i].Initialize(decay_delay_time, 0.0f);
+    }
+
+    const auto pre_delay{long_size_pre_delay_supported ? 350.0f : 150.0f};
+    const auto pre_delay_line{(pre_delay * delay).to_uint_floor()};
+    state.pre_delay_line.Initialize(pre_delay_line, 1.0f);
+
+    const auto center_delay_time{(5 * delay).to_uint_floor()};
+    state.center_delay_line.Initialize(center_delay_time, 1.0f);
+
+    UpdateReverbEffectParameter(params, state);
+
+    for (u32 i = 0; i < ReverbInfo::MaxDelayLines; i++) {
+        std::ranges::fill(state.fdn_delay_lines[i].buffer, 0);
+        std::ranges::fill(state.decay_delay_lines[i].buffer, 0);
+    }
+    std::ranges::fill(state.center_delay_line.buffer, 0);
+    std::ranges::fill(state.pre_delay_line.buffer, 0);
+}
+
+/**
+ * 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.
+ */
+static void ApplyReverbEffectBypass(std::span<std::span<const s32>> inputs,
+                                    std::span<std::span<s32>> outputs, const u32 channel_count,
+                                    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 decay  - The 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(ReverbInfo::ReverbDelayLine& decay,
+                                                   ReverbInfo::ReverbDelayLine& fdn,
+                                                   const Common::FixedPoint<50, 14> mix) {
+    const auto val{decay.Read()};
+    const auto mixed{mix - (val * decay.decay)};
+    const auto out{decay.Tick(mixed) + (mixed * decay.decay)};
+
+    fdn.Tick(out);
+    return out;
+}
+
+/**
+ * Impl. Apply a 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 params       - Input parameters to update the state.
+ * @param state        - State to use, must be initialized (see InitializeReverbEffect).
+ * @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 ApplyReverbEffect(const ReverbInfo::ParameterVersion2& params, ReverbInfo::State& state,
+                              std::vector<std::span<const s32>>& inputs,
+                              std::vector<std::span<s32>>& outputs, const u32 sample_count) {
+    constexpr std::array<u8, ReverbInfo::MaxDelayTaps> OutTapIndexes1Ch{
+        0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+    };
+    constexpr std::array<u8, ReverbInfo::MaxDelayTaps> OutTapIndexes2Ch{
+        0, 0, 1, 1, 0, 1, 0, 0, 1, 1,
+    };
+    constexpr std::array<u8, ReverbInfo::MaxDelayTaps> OutTapIndexes4Ch{
+        0, 0, 1, 1, 0, 1, 2, 2, 3, 3,
+    };
+    constexpr std::array<u8, ReverbInfo::MaxDelayTaps> OutTapIndexes6Ch{
+        0, 0, 1, 1, 2, 2, 4, 4, 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++) {
+        std::array<Common::FixedPoint<50, 14>, NumChannels> output_samples{};
+
+        for (u32 early_tap = 0; early_tap < ReverbInfo::MaxDelayTaps; early_tap++) {
+            const auto sample{state.pre_delay_line.TapOut(state.early_delay_times[early_tap]) *
+                              state.early_gains[early_tap]};
+            output_samples[tap_indexes[early_tap]] += sample;
+            if constexpr (NumChannels == 6) {
+                output_samples[static_cast<u32>(Channels::LFE)] += sample;
+            }
+        }
+
+        if constexpr (NumChannels == 6) {
+            output_samples[static_cast<u32>(Channels::LFE)] *= 0.2f;
+        }
+
+        Common::FixedPoint<50, 14> input_sample{};
+        for (u32 channel = 0; channel < NumChannels; channel++) {
+            input_sample += inputs[channel][sample_index];
+        }
+
+        input_sample *= 64;
+        input_sample *= Common::FixedPoint<50, 14>::from_base(params.base_gain);
+        state.pre_delay_line.Write(input_sample);
+
+        for (u32 i = 0; i < ReverbInfo::MaxDelayLines; i++) {
+            state.prev_feedback_output[i] =
+                state.prev_feedback_output[i] * state.hf_decay_prev_gain[i] +
+                state.fdn_delay_lines[i].Read() * state.hf_decay_gain[i];
+        }
+
+        Common::FixedPoint<50, 14> pre_delay_sample{
+            state.pre_delay_line.Read() * Common::FixedPoint<50, 14>::from_base(params.late_gain)};
+
+        std::array<Common::FixedPoint<50, 14>, ReverbInfo::MaxDelayLines> mix_matrix{
+            state.prev_feedback_output[2] + state.prev_feedback_output[1] + pre_delay_sample,
+            -state.prev_feedback_output[0] - state.prev_feedback_output[3] + pre_delay_sample,
+            state.prev_feedback_output[0] - state.prev_feedback_output[3] + pre_delay_sample,
+            state.prev_feedback_output[1] - state.prev_feedback_output[2] + pre_delay_sample,
+        };
+
+        std::array<Common::FixedPoint<50, 14>, ReverbInfo::MaxDelayLines> allpass_samples{};
+        for (u32 i = 0; i < ReverbInfo::MaxDelayLines; i++) {
+            allpass_samples[i] = Axfx2AllPassTick(state.decay_delay_lines[i],
+                                                  state.fdn_delay_lines[i], mix_matrix[i]);
+        }
+
+        const auto dry_gain{Common::FixedPoint<50, 14>::from_base(params.dry_gain)};
+        const auto wet_gain{Common::FixedPoint<50, 14>::from_base(params.wet_gain)};
+
+        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++) {
+                auto in_sample{inputs[channel][sample_index] * dry_gain};
+
+                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) * wet_gain) / 64};
+                outputs[channel][sample_index] = (in_sample + out_sample).to_int();
+            }
+        } else {
+            for (u32 channel = 0; channel < NumChannels; channel++) {
+                auto in_sample{inputs[channel][sample_index] * dry_gain};
+                auto out_sample{((output_samples[channel] + allpass_samples[channel]) * wet_gain) /
+                                64};
+                outputs[channel][sample_index] = (in_sample + out_sample).to_int();
+            }
+        }
+    }
+}
+
+/**
+ * Apply a 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 InitializeReverbEffect).
+ * @param enabled      - If enabled, delay will be applied, otherwise input is copied to output.
+ * @param inputs       - Input mix buffers to performan the reverb on.
+ * @param outputs      - Output mix buffers to receive the reverbed samples.
+ * @param sample_count - Number of samples to process.
+ */
+static void ApplyReverbEffect(const ReverbInfo::ParameterVersion2& params, ReverbInfo::State& state,
+                              const bool enabled, std::vector<std::span<const s32>>& inputs,
+                              std::vector<std::span<s32>>& outputs, const u32 sample_count) {
+    if (enabled) {
+        switch (params.channel_count) {
+        case 0:
+            return;
+        case 1:
+            ApplyReverbEffect<1>(params, state, inputs, outputs, sample_count);
+            break;
+        case 2:
+            ApplyReverbEffect<2>(params, state, inputs, outputs, sample_count);
+            break;
+        case 4:
+            ApplyReverbEffect<4>(params, state, inputs, outputs, sample_count);
+            break;
+        case 6:
+            ApplyReverbEffect<6>(params, state, inputs, outputs, sample_count);
+            break;
+        default:
+            ApplyReverbEffectBypass(inputs, outputs, params.channel_count, sample_count);
+            break;
+        }
+    } else {
+        ApplyReverbEffectBypass(inputs, outputs, params.channel_count, sample_count);
+    }
+}
+
+void ReverbCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor,
+                         std::string& string) {
+    string += fmt::format(
+        "ReverbCommand\n\tenabled {} long_size_pre_delay_supported {}\n\tinputs: ", effect_enabled,
+        long_size_pre_delay_supported);
+    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 ReverbCommand::Process(const ADSP::CommandListProcessor& processor) {
+    std::vector<std::span<const s32>> input_buffers(parameter.channel_count);
+    std::vector<std::span<s32>> output_buffers(parameter.channel_count);
+
+    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<ReverbInfo::State*>(state)};
+
+    if (effect_enabled) {
+        if (parameter.state == ReverbInfo::ParameterState::Updating) {
+            UpdateReverbEffectParameter(parameter, *state_);
+        } else if (parameter.state == ReverbInfo::ParameterState::Initialized) {
+            InitializeReverbEffect(parameter, *state_, workbuffer, long_size_pre_delay_supported);
+        }
+    }
+    ApplyReverbEffect(parameter, *state_, effect_enabled, input_buffers, output_buffers,
+                      processor.sample_count);
+}
+
+bool ReverbCommand::Verify(const ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/effect/reverb.h b/src/audio_core/renderer/command/effect/reverb.h
new file mode 100644
index 0000000000..328756150f
--- /dev/null
+++ b/src/audio_core/renderer/command/effect/reverb.h
@@ -0,0 +1,62 @@
+// 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/reverb.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * AudioRenderer command for a Reverb effect. Apply a reverb to inputs mix buffer, outputs receives
+ * the results.
+ */
+struct ReverbCommand : 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::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
+    ReverbInfo::ParameterVersion2 parameter;
+    /// State, updated each call
+    CpuAddr state;
+    /// Game-supplied workbuffer (Unused)
+    CpuAddr workbuffer;
+    /// Is this effect enabled?
+    bool effect_enabled;
+    /// Is a longer pre-delay time supported?
+    bool long_size_pre_delay_supported;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/icommand.h b/src/audio_core/renderer/command/icommand.h
new file mode 100644
index 0000000000..f2dd412541
--- /dev/null
+++ b/src/audio_core/renderer/command/icommand.h
@@ -0,0 +1,93 @@
+// 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 {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+enum class CommandId : u8 {
+    /* 0x00 */ Invalid,
+    /* 0x01 */ DataSourcePcmInt16Version1,
+    /* 0x02 */ DataSourcePcmInt16Version2,
+    /* 0x03 */ DataSourcePcmFloatVersion1,
+    /* 0x04 */ DataSourcePcmFloatVersion2,
+    /* 0x05 */ DataSourceAdpcmVersion1,
+    /* 0x06 */ DataSourceAdpcmVersion2,
+    /* 0x07 */ Volume,
+    /* 0x08 */ VolumeRamp,
+    /* 0x09 */ BiquadFilter,
+    /* 0x0A */ Mix,
+    /* 0x0B */ MixRamp,
+    /* 0x0C */ MixRampGrouped,
+    /* 0x0D */ DepopPrepare,
+    /* 0x0E */ DepopForMixBuffers,
+    /* 0x0F */ Delay,
+    /* 0x10 */ Upsample,
+    /* 0x11 */ DownMix6chTo2ch,
+    /* 0x12 */ Aux,
+    /* 0x13 */ DeviceSink,
+    /* 0x14 */ CircularBufferSink,
+    /* 0x15 */ Reverb,
+    /* 0x16 */ I3dl2Reverb,
+    /* 0x17 */ Performance,
+    /* 0x18 */ ClearMixBuffer,
+    /* 0x19 */ CopyMixBuffer,
+    /* 0x1A */ LightLimiterVersion1,
+    /* 0x1B */ LightLimiterVersion2,
+    /* 0x1C */ MultiTapBiquadFilter,
+    /* 0x1D */ Capture,
+    /* 0x1E */ Compressor,
+};
+
+constexpr u32 CommandMagic{0xCAFEBABE};
+
+/**
+ * A command, generated by the host, and processed by the ADSP's AudioRenderer.
+ */
+struct ICommand {
+    virtual ~ICommand() = default;
+
+    /**
+     * Print this command's information to a string.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     * @param string    - The string to print into.
+     */
+    virtual void Dump(const ADSP::CommandListProcessor& processor, std::string& string) = 0;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    virtual void Process(const ADSP::CommandListProcessor& processor) = 0;
+
+    /**
+     * Verify this command's data is valid.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     * @return True if the command is valid, otherwise false.
+     */
+    virtual bool Verify(const ADSP::CommandListProcessor& processor) = 0;
+
+    /// Command magic 0xCAFEBABE
+    u32 magic{};
+    /// Command enabled
+    bool enabled{};
+    /// Type of this command (see CommandId)
+    CommandId type{};
+    /// Size of this command
+    s16 size{};
+    /// Estimated processing time for this command
+    u32 estimated_process_time{};
+    /// Node id of the voice or mix this command was generated from
+    u32 node_id{};
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/mix/clear_mix.cpp b/src/audio_core/renderer/command/mix/clear_mix.cpp
new file mode 100644
index 0000000000..4f649d6a87
--- /dev/null
+++ b/src/audio_core/renderer/command/mix/clear_mix.cpp
@@ -0,0 +1,24 @@
+// 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/mix/clear_mix.h"
+
+namespace AudioCore::AudioRenderer {
+
+void ClearMixBufferCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor,
+                                 std::string& string) {
+    string += fmt::format("ClearMixBufferCommand\n");
+}
+
+void ClearMixBufferCommand::Process(const ADSP::CommandListProcessor& processor) {
+    memset(processor.mix_buffers.data(), 0, processor.mix_buffers.size_bytes());
+}
+
+bool ClearMixBufferCommand::Verify(const ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/mix/clear_mix.h b/src/audio_core/renderer/command/mix/clear_mix.h
new file mode 100644
index 0000000000..956ec0b65b
--- /dev/null
+++ b/src/audio_core/renderer/command/mix/clear_mix.h
@@ -0,0 +1,45 @@
+// 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::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * AudioRenderer command for a clearing the mix buffers.
+ * Used at the start of each command list.
+ */
+struct ClearMixBufferCommand : 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::CommandListProcessor& processor) override;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/mix/copy_mix.cpp b/src/audio_core/renderer/command/mix/copy_mix.cpp
new file mode 100644
index 0000000000..1d49f1644f
--- /dev/null
+++ b/src/audio_core/renderer/command/mix/copy_mix.cpp
@@ -0,0 +1,27 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/adsp/command_list_processor.h"
+#include "audio_core/renderer/command/mix/copy_mix.h"
+
+namespace AudioCore::AudioRenderer {
+
+void CopyMixBufferCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor,
+                                std::string& string) {
+    string += fmt::format("CopyMixBufferCommand\n\tinput {:02X} output {:02X}\n", input_index,
+                          output_index);
+}
+
+void CopyMixBufferCommand::Process(const ADSP::CommandListProcessor& processor) {
+    auto output{processor.mix_buffers.subspan(output_index * processor.sample_count,
+                                              processor.sample_count)};
+    auto input{processor.mix_buffers.subspan(input_index * processor.sample_count,
+                                             processor.sample_count)};
+    std::memcpy(output.data(), input.data(), processor.sample_count * sizeof(s32));
+}
+
+bool CopyMixBufferCommand::Verify(const ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/mix/copy_mix.h b/src/audio_core/renderer/command/mix/copy_mix.h
new file mode 100644
index 0000000000..a59007fb63
--- /dev/null
+++ b/src/audio_core/renderer/command/mix/copy_mix.h
@@ -0,0 +1,49 @@
+// 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::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * AudioRenderer command for a copying a mix buffer from input to output.
+ */
+struct CopyMixBufferCommand : 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::CommandListProcessor& processor) override;
+
+    /// Input mix buffer index
+    s16 input_index;
+    /// Output mix buffer index
+    s16 output_index;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/mix/depop_for_mix_buffers.cpp b/src/audio_core/renderer/command/mix/depop_for_mix_buffers.cpp
new file mode 100644
index 0000000000..c2bc10061c
--- /dev/null
+++ b/src/audio_core/renderer/command/mix/depop_for_mix_buffers.cpp
@@ -0,0 +1,64 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/common/common.h"
+#include "audio_core/renderer/adsp/command_list_processor.h"
+#include "audio_core/renderer/command/mix/depop_for_mix_buffers.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * Apply depopping. Add the depopped sample to each incoming new sample, decaying it each time
+ * according to decay.
+ *
+ * @param output - Output buffer to be depopped.
+ * @param depop_sample - Depopped sample to apply to output samples.
+ * @param decay_ - Amount to decay the depopped sample for every output sample.
+ * @param sample_count - Samples to process.
+ * @return Final decayed depop sample.
+ */
+static s32 ApplyDepopMix(std::span<s32> output, const s32 depop_sample,
+                         Common::FixedPoint<49, 15>& decay_, const u32 sample_count) {
+    auto sample{std::abs(depop_sample)};
+    auto decay{decay_.to_raw()};
+
+    if (depop_sample <= 0) {
+        for (u32 i = 0; i < sample_count; i++) {
+            sample = static_cast<s32>((static_cast<s64>(sample) * decay) >> 15);
+            output[i] -= sample;
+        }
+        return -sample;
+    } else {
+        for (u32 i = 0; i < sample_count; i++) {
+            sample = static_cast<s32>((static_cast<s64>(sample) * decay) >> 15);
+            output[i] += sample;
+        }
+        return sample;
+    }
+}
+
+void DepopForMixBuffersCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor,
+                                     std::string& string) {
+    string += fmt::format("DepopForMixBuffersCommand\n\tinput {:02X} count {} decay {}\n", input,
+                          count, decay.to_float());
+}
+
+void DepopForMixBuffersCommand::Process(const ADSP::CommandListProcessor& processor) {
+    auto end_index{std::min(processor.buffer_count, input + count)};
+    std::span<s32> depop_buff{reinterpret_cast<s32*>(depop_buffer), end_index};
+
+    for (u32 index = input; index < end_index; index++) {
+        const auto depop_sample{depop_buff[index]};
+        if (depop_sample != 0) {
+            auto input_buffer{processor.mix_buffers.subspan(index * processor.sample_count,
+                                                            processor.sample_count)};
+            depop_buff[index] =
+                ApplyDepopMix(input_buffer, depop_sample, decay, processor.sample_count);
+        }
+    }
+}
+
+bool DepopForMixBuffersCommand::Verify(const ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/mix/depop_for_mix_buffers.h b/src/audio_core/renderer/command/mix/depop_for_mix_buffers.h
new file mode 100644
index 0000000000..e7268ff278
--- /dev/null
+++ b/src/audio_core/renderer/command/mix/depop_for_mix_buffers.h
@@ -0,0 +1,55 @@
+// 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"
+#include "common/fixed_point.h"
+
+namespace AudioCore::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * AudioRenderer command for depopping a mix buffer.
+ * Adds a cumulation of previous samples to the current mix buffer with a decay.
+ */
+struct DepopForMixBuffersCommand : 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::CommandListProcessor& processor) override;
+
+    /// Starting input mix buffer index
+    u32 input;
+    /// Number of mix buffers to depop
+    u32 count;
+    /// Amount to decay the depop sample for each new sample
+    Common::FixedPoint<49, 15> decay;
+    /// Address of the depop buffer, holding the last sample for every mix buffer
+    CpuAddr depop_buffer;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/mix/depop_prepare.cpp b/src/audio_core/renderer/command/mix/depop_prepare.cpp
new file mode 100644
index 0000000000..2ee076ef6b
--- /dev/null
+++ b/src/audio_core/renderer/command/mix/depop_prepare.cpp
@@ -0,0 +1,36 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/adsp/command_list_processor.h"
+#include "audio_core/renderer/command/mix/depop_prepare.h"
+#include "audio_core/renderer/voice/voice_state.h"
+#include "common/fixed_point.h"
+
+namespace AudioCore::AudioRenderer {
+
+void DepopPrepareCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor,
+                               std::string& string) {
+    string += fmt::format("DepopPrepareCommand\n\tinputs: ");
+    for (u32 i = 0; i < buffer_count; i++) {
+        string += fmt::format("{:02X}, ", inputs[i]);
+    }
+    string += "\n";
+}
+
+void DepopPrepareCommand::Process(const ADSP::CommandListProcessor& processor) {
+    auto samples{reinterpret_cast<s32*>(previous_samples)};
+    auto buffer{std::span(reinterpret_cast<s32*>(depop_buffer), buffer_count)};
+
+    for (u32 i = 0; i < buffer_count; i++) {
+        if (samples[i]) {
+            buffer[inputs[i]] += samples[i];
+            samples[i] = 0;
+        }
+    }
+}
+
+bool DepopPrepareCommand::Verify(const ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/mix/depop_prepare.h b/src/audio_core/renderer/command/mix/depop_prepare.h
new file mode 100644
index 0000000000..a5465da9ae
--- /dev/null
+++ b/src/audio_core/renderer/command/mix/depop_prepare.h
@@ -0,0 +1,54 @@
+// 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::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * AudioRenderer command for preparing depop.
+ * Adds the previusly output last samples to the depop buffer.
+ */
+struct DepopPrepareCommand : 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::CommandListProcessor& processor) override;
+
+    /// Depop buffer offset for each mix buffer
+    std::array<s16, MaxMixBuffers> inputs;
+    /// Pointer to the previous mix buffer samples
+    CpuAddr previous_samples;
+    /// Number of mix buffers to use
+    u32 buffer_count;
+    /// Pointer to the current depop values
+    CpuAddr depop_buffer;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/mix/mix.cpp b/src/audio_core/renderer/command/mix/mix.cpp
new file mode 100644
index 0000000000..8ecf9b05a9
--- /dev/null
+++ b/src/audio_core/renderer/command/mix/mix.cpp
@@ -0,0 +1,70 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <algorithm>
+#include <limits>
+#include <span>
+
+#include "audio_core/renderer/adsp/command_list_processor.h"
+#include "audio_core/renderer/command/mix/mix.h"
+#include "common/fixed_point.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * Mix input mix buffer into output mix buffer, with volume applied to the input.
+ *
+ * @tparam Q           - Number of bits for fixed point operations.
+ * @param output       - Output mix buffer.
+ * @param input        - Input mix buffer.
+ * @param volume       - Volume applied to the input.
+ * @param sample_count - Number of samples to process.
+ */
+template <size_t Q>
+static void ApplyMix(std::span<s32> output, std::span<const s32> input, const f32 volume_,
+                     const u32 sample_count) {
+    const Common::FixedPoint<64 - Q, Q> volume{volume_};
+    for (u32 i = 0; i < sample_count; i++) {
+        output[i] = (output[i] + input[i] * volume).to_int();
+    }
+}
+
+void MixCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor,
+                      std::string& string) {
+    string += fmt::format("MixCommand");
+    string += fmt::format("\n\tinput {:02X}", input_index);
+    string += fmt::format("\n\toutput {:02X}", output_index);
+    string += fmt::format("\n\tvolume {:.8f}", volume);
+    string += "\n";
+}
+
+void MixCommand::Process(const ADSP::CommandListProcessor& processor) {
+    auto output{processor.mix_buffers.subspan(output_index * processor.sample_count,
+                                              processor.sample_count)};
+    auto input{processor.mix_buffers.subspan(input_index * processor.sample_count,
+                                             processor.sample_count)};
+
+    // If volume is 0, nothing will be added to the output, so just skip.
+    if (volume == 0.0f) {
+        return;
+    }
+
+    switch (precision) {
+    case 15:
+        ApplyMix<15>(output, input, volume, processor.sample_count);
+        break;
+
+    case 23:
+        ApplyMix<23>(output, input, volume, processor.sample_count);
+        break;
+
+    default:
+        LOG_ERROR(Service_Audio, "Invalid precision {}", precision);
+        break;
+    }
+}
+
+bool MixCommand::Verify(const ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/mix/mix.h b/src/audio_core/renderer/command/mix/mix.h
new file mode 100644
index 0000000000..0201cf1718
--- /dev/null
+++ b/src/audio_core/renderer/command/mix/mix.h
@@ -0,0 +1,54 @@
+// 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::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * AudioRenderer command for mixing an input mix buffer to an output mix buffer, with a volume
+ * applied to the input.
+ */
+struct MixCommand : 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::CommandListProcessor& processor) override;
+
+    /// Fixed point precision
+    u8 precision;
+    /// Input mix buffer index
+    s16 input_index;
+    /// Output mix buffer index
+    s16 output_index;
+    /// Mix volume applied to the input
+    f32 volume;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/mix/mix_ramp.cpp b/src/audio_core/renderer/command/mix/mix_ramp.cpp
new file mode 100644
index 0000000000..ffdafa1c8d
--- /dev/null
+++ b/src/audio_core/renderer/command/mix/mix_ramp.cpp
@@ -0,0 +1,94 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/adsp/command_list_processor.h"
+#include "audio_core/renderer/command/mix/mix_ramp.h"
+#include "common/fixed_point.h"
+#include "common/logging/log.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * Mix input mix buffer into output mix buffer, with volume applied to the input.
+ *
+ * @tparam Q           - Number of bits for fixed point operations.
+ * @param output       - Output mix buffer.
+ * @param input        - Input mix buffer.
+ * @param volume       - Volume applied to the input.
+ * @param ramp         - Ramp applied to volume every sample.
+ * @param sample_count - Number of samples to process.
+ * @return The final gained input sample, used for depopping.
+ */
+template <size_t Q>
+s32 ApplyMixRamp(std::span<s32> output, std::span<const s32> input, const f32 volume_,
+                 const f32 ramp_, const u32 sample_count) {
+    Common::FixedPoint<64 - Q, Q> volume{volume_};
+    Common::FixedPoint<64 - Q, Q> sample{0};
+
+    if (ramp_ == 0.0f) {
+        for (u32 i = 0; i < sample_count; i++) {
+            sample = input[i] * volume;
+            output[i] = (output[i] + sample).to_int();
+        }
+    } else {
+        Common::FixedPoint<64 - Q, Q> ramp{ramp_};
+        for (u32 i = 0; i < sample_count; i++) {
+            sample = input[i] * volume;
+            output[i] = (output[i] + sample).to_int();
+            volume += ramp;
+        }
+    }
+    return sample.to_int();
+}
+
+template s32 ApplyMixRamp<15>(std::span<s32>, std::span<const s32>, const f32, const f32,
+                              const u32);
+template s32 ApplyMixRamp<23>(std::span<s32>, std::span<const s32>, const f32, const f32,
+                              const u32);
+
+void MixRampCommand::Dump(const ADSP::CommandListProcessor& processor, std::string& string) {
+    const auto ramp{(volume - prev_volume) / static_cast<f32>(processor.sample_count)};
+    string += fmt::format("MixRampCommand");
+    string += fmt::format("\n\tinput {:02X}", input_index);
+    string += fmt::format("\n\toutput {:02X}", output_index);
+    string += fmt::format("\n\tvolume {:.8f}", volume);
+    string += fmt::format("\n\tprev_volume {:.8f}", prev_volume);
+    string += fmt::format("\n\tramp {:.8f}", ramp);
+    string += "\n";
+}
+
+void MixRampCommand::Process(const ADSP::CommandListProcessor& processor) {
+    auto output{processor.mix_buffers.subspan(output_index * processor.sample_count,
+                                              processor.sample_count)};
+    auto input{processor.mix_buffers.subspan(input_index * processor.sample_count,
+                                             processor.sample_count)};
+    const auto ramp{(volume - prev_volume) / static_cast<f32>(processor.sample_count)};
+    auto prev_sample_ptr{reinterpret_cast<s32*>(previous_sample)};
+
+    // If previous volume and ramp are both 0, nothing will be added to the output, so just skip.
+    if (prev_volume == 0.0f && ramp == 0.0f) {
+        *prev_sample_ptr = 0;
+        return;
+    }
+
+    switch (precision) {
+    case 15:
+        *prev_sample_ptr =
+            ApplyMixRamp<15>(output, input, prev_volume, ramp, processor.sample_count);
+        break;
+
+    case 23:
+        *prev_sample_ptr =
+            ApplyMixRamp<23>(output, input, prev_volume, ramp, processor.sample_count);
+        break;
+
+    default:
+        LOG_ERROR(Service_Audio, "Invalid precision {}", precision);
+        break;
+    }
+}
+
+bool MixRampCommand::Verify(const ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/mix/mix_ramp.h b/src/audio_core/renderer/command/mix/mix_ramp.h
new file mode 100644
index 0000000000..770f57e802
--- /dev/null
+++ b/src/audio_core/renderer/command/mix/mix_ramp.h
@@ -0,0 +1,73 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <span>
+#include <string>
+
+#include "audio_core/renderer/command/icommand.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * AudioRenderer command for mixing an input mix buffer to an output mix buffer, with a volume
+ * applied to the input, and volume ramping to smooth out the transition.
+ */
+struct MixRampCommand : 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::CommandListProcessor& processor) override;
+
+    /// Fixed point precision
+    u8 precision;
+    /// Input mix buffer index
+    s16 input_index;
+    /// Output mix buffer index
+    s16 output_index;
+    /// Previous mix volume
+    f32 prev_volume;
+    /// Current mix volume
+    f32 volume;
+    /// Pointer to the previous sample buffer, used for depopping
+    CpuAddr previous_sample;
+};
+
+/**
+ * Mix input mix buffer into output mix buffer, with volume applied to the input.
+ * @tparam Q           - Number of bits for fixed point operations.
+ * @param output       - Output mix buffer.
+ * @param input        - Input mix buffer.
+ * @param volume       - Volume applied to the input.
+ * @param ramp         - Ramp applied to volume every sample.
+ * @param sample_count - Number of samples to process.
+ * @return The final gained input sample, used for depopping.
+ */
+template <size_t Q>
+s32 ApplyMixRamp(std::span<s32> output, std::span<const s32> input, const f32 volume_,
+                 const f32 ramp_, const u32 sample_count);
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/mix/mix_ramp_grouped.cpp b/src/audio_core/renderer/command/mix/mix_ramp_grouped.cpp
new file mode 100644
index 0000000000..43dbef9fca
--- /dev/null
+++ b/src/audio_core/renderer/command/mix/mix_ramp_grouped.cpp
@@ -0,0 +1,65 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/adsp/command_list_processor.h"
+#include "audio_core/renderer/command/mix/mix_ramp.h"
+#include "audio_core/renderer/command/mix/mix_ramp_grouped.h"
+
+namespace AudioCore::AudioRenderer {
+
+void MixRampGroupedCommand::Dump(const ADSP::CommandListProcessor& processor, std::string& string) {
+    string += "MixRampGroupedCommand";
+    for (u32 i = 0; i < buffer_count; i++) {
+        string += fmt::format("\n\t{}", i);
+        const auto ramp{(volumes[i] - prev_volumes[i]) / static_cast<f32>(processor.sample_count)};
+        string += fmt::format("\n\t\tinput {:02X}", inputs[i]);
+        string += fmt::format("\n\t\toutput {:02X}", outputs[i]);
+        string += fmt::format("\n\t\tvolume {:.8f}", volumes[i]);
+        string += fmt::format("\n\t\tprev_volume {:.8f}", prev_volumes[i]);
+        string += fmt::format("\n\t\tramp {:.8f}", ramp);
+        string += "\n";
+    }
+}
+
+void MixRampGroupedCommand::Process(const ADSP::CommandListProcessor& processor) {
+    std::span<s32> prev_samples = {reinterpret_cast<s32*>(previous_samples), MaxMixBuffers};
+
+    for (u32 i = 0; i < buffer_count; i++) {
+        auto last_sample{0};
+        if (prev_volumes[i] != 0.0f || volumes[i] != 0.0f) {
+            const auto output{processor.mix_buffers.subspan(outputs[i] * processor.sample_count,
+                                                            processor.sample_count)};
+            const auto input{processor.mix_buffers.subspan(inputs[i] * processor.sample_count,
+                                                           processor.sample_count)};
+            const auto ramp{(volumes[i] - prev_volumes[i]) /
+                            static_cast<f32>(processor.sample_count)};
+
+            if (prev_volumes[i] == 0.0f && ramp == 0.0f) {
+                prev_samples[i] = 0;
+                continue;
+            }
+
+            switch (precision) {
+            case 15:
+                last_sample =
+                    ApplyMixRamp<15>(output, input, prev_volumes[i], ramp, processor.sample_count);
+                break;
+            case 23:
+                last_sample =
+                    ApplyMixRamp<23>(output, input, prev_volumes[i], ramp, processor.sample_count);
+                break;
+            default:
+                LOG_ERROR(Service_Audio, "Invalid precision {}", precision);
+                break;
+            }
+        }
+
+        prev_samples[i] = last_sample;
+    }
+}
+
+bool MixRampGroupedCommand::Verify(const ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/mix/mix_ramp_grouped.h b/src/audio_core/renderer/command/mix/mix_ramp_grouped.h
new file mode 100644
index 0000000000..027276e5a4
--- /dev/null
+++ b/src/audio_core/renderer/command/mix/mix_ramp_grouped.h
@@ -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 "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * AudioRenderer command for mixing multiple input mix buffers to multiple output mix buffers, with
+ * a volume applied to the input, and volume ramping to smooth out the transition.
+ */
+struct MixRampGroupedCommand : 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::CommandListProcessor& processor) override;
+
+    /// Fixed point precision
+    u8 precision;
+    /// Number of mix buffers to mix
+    u32 buffer_count;
+    /// Input mix buffer indexes for each mix buffer
+    std::array<s16, MaxMixBuffers> inputs;
+    /// Output mix buffer indexes for each mix buffer
+    std::array<s16, MaxMixBuffers> outputs;
+    /// Previous mix vloumes for each mix buffer
+    std::array<f32, MaxMixBuffers> prev_volumes;
+    /// Current mix vloumes for each mix buffer
+    std::array<f32, MaxMixBuffers> volumes;
+    /// Pointer to the previous sample buffer, used for depop
+    CpuAddr previous_samples;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/mix/volume.cpp b/src/audio_core/renderer/command/mix/volume.cpp
new file mode 100644
index 0000000000..b045fb0625
--- /dev/null
+++ b/src/audio_core/renderer/command/mix/volume.cpp
@@ -0,0 +1,72 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/adsp/command_list_processor.h"
+#include "audio_core/renderer/command/mix/volume.h"
+#include "common/fixed_point.h"
+#include "common/logging/log.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * Apply volume to the input mix buffer, saving to the output buffer.
+ *
+ * @tparam Q           - Number of bits for fixed point operations.
+ * @param output       - Output mix buffer.
+ * @param input        - Input mix buffer.
+ * @param volume       - Volume applied to the input.
+ * @param sample_count - Number of samples to process.
+ */
+template <size_t Q>
+static void ApplyUniformGain(std::span<s32> output, std::span<const s32> input, const f32 volume,
+                             const u32 sample_count) {
+    if (volume == 1.0f) {
+        std::memcpy(output.data(), input.data(), input.size_bytes());
+    } else {
+        const Common::FixedPoint<64 - Q, Q> gain{volume};
+        for (u32 i = 0; i < sample_count; i++) {
+            output[i] = (input[i] * gain).to_int();
+        }
+    }
+}
+
+void VolumeCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor,
+                         std::string& string) {
+    string += fmt::format("VolumeCommand");
+    string += fmt::format("\n\tinput {:02X}", input_index);
+    string += fmt::format("\n\toutput {:02X}", output_index);
+    string += fmt::format("\n\tvolume {:.8f}", volume);
+    string += "\n";
+}
+
+void VolumeCommand::Process(const ADSP::CommandListProcessor& processor) {
+    // If input and output buffers are the same, and the volume is 1.0f, this won't do
+    // anything, so just skip.
+    if (input_index == output_index && volume == 1.0f) {
+        return;
+    }
+
+    auto output{processor.mix_buffers.subspan(output_index * processor.sample_count,
+                                              processor.sample_count)};
+    auto input{processor.mix_buffers.subspan(input_index * processor.sample_count,
+                                             processor.sample_count)};
+
+    switch (precision) {
+    case 15:
+        ApplyUniformGain<15>(output, input, volume, processor.sample_count);
+        break;
+
+    case 23:
+        ApplyUniformGain<23>(output, input, volume, processor.sample_count);
+        break;
+
+    default:
+        LOG_ERROR(Service_Audio, "Invalid precision {}", precision);
+        break;
+    }
+}
+
+bool VolumeCommand::Verify(const ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/mix/volume.h b/src/audio_core/renderer/command/mix/volume.h
new file mode 100644
index 0000000000..6ae9fb7943
--- /dev/null
+++ b/src/audio_core/renderer/command/mix/volume.h
@@ -0,0 +1,53 @@
+// 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::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * AudioRenderer command for applying volume to a mix buffer.
+ */
+struct VolumeCommand : 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::CommandListProcessor& processor) override;
+
+    /// Fixed point precision
+    u8 precision;
+    /// Input mix buffer index
+    s16 input_index;
+    /// Output mix buffer index
+    s16 output_index;
+    /// Mix volume applied to the input
+    f32 volume;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/mix/volume_ramp.cpp b/src/audio_core/renderer/command/mix/volume_ramp.cpp
new file mode 100644
index 0000000000..4243071481
--- /dev/null
+++ b/src/audio_core/renderer/command/mix/volume_ramp.cpp
@@ -0,0 +1,84 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/adsp/command_list_processor.h"
+#include "audio_core/renderer/command/mix/volume_ramp.h"
+#include "common/fixed_point.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * Apply volume with ramping to the input mix buffer, saving to the output buffer.
+ *
+ * @tparam Q           - Number of bits for fixed point operations.
+ * @param output       - Output mix buffers.
+ * @param input        - Input mix buffers.
+ * @param volume       - Volume applied to the input.
+ * @param ramp         - Ramp applied to volume every sample.
+ * @param sample_count - Number of samples to process.
+ */
+template <size_t Q>
+static void ApplyLinearEnvelopeGain(std::span<s32> output, std::span<const s32> input,
+                                    const f32 volume, const f32 ramp_, const u32 sample_count) {
+    if (volume == 0.0f && ramp_ == 0.0f) {
+        std::memset(output.data(), 0, output.size_bytes());
+    } else if (volume == 1.0f && ramp_ == 0.0f) {
+        std::memcpy(output.data(), input.data(), output.size_bytes());
+    } else if (ramp_ == 0.0f) {
+        const Common::FixedPoint<64 - Q, Q> gain{volume};
+        for (u32 i = 0; i < sample_count; i++) {
+            output[i] = (input[i] * gain).to_int();
+        }
+    } else {
+        Common::FixedPoint<64 - Q, Q> gain{volume};
+        const Common::FixedPoint<64 - Q, Q> ramp{ramp_};
+        for (u32 i = 0; i < sample_count; i++) {
+            output[i] = (input[i] * gain).to_int();
+            gain += ramp;
+        }
+    }
+}
+
+void VolumeRampCommand::Dump(const ADSP::CommandListProcessor& processor, std::string& string) {
+    const auto ramp{(volume - prev_volume) / static_cast<f32>(processor.sample_count)};
+    string += fmt::format("VolumeRampCommand");
+    string += fmt::format("\n\tinput {:02X}", input_index);
+    string += fmt::format("\n\toutput {:02X}", output_index);
+    string += fmt::format("\n\tvolume {:.8f}", volume);
+    string += fmt::format("\n\tprev_volume {:.8f}", prev_volume);
+    string += fmt::format("\n\tramp {:.8f}", ramp);
+    string += "\n";
+}
+
+void VolumeRampCommand::Process(const ADSP::CommandListProcessor& processor) {
+    auto output{processor.mix_buffers.subspan(output_index * processor.sample_count,
+                                              processor.sample_count)};
+    auto input{processor.mix_buffers.subspan(input_index * processor.sample_count,
+                                             processor.sample_count)};
+    const auto ramp{(volume - prev_volume) / static_cast<f32>(processor.sample_count)};
+
+    // If input and output buffers are the same, and the volume is 1.0f, and there's no ramping,
+    // this won't do anything, so just skip.
+    if (input_index == output_index && prev_volume == 1.0f && ramp == 0.0f) {
+        return;
+    }
+
+    switch (precision) {
+    case 15:
+        ApplyLinearEnvelopeGain<15>(output, input, prev_volume, ramp, processor.sample_count);
+        break;
+
+    case 23:
+        ApplyLinearEnvelopeGain<23>(output, input, prev_volume, ramp, processor.sample_count);
+        break;
+
+    default:
+        LOG_ERROR(Service_Audio, "Invalid precision {}", precision);
+        break;
+    }
+}
+
+bool VolumeRampCommand::Verify(const ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/mix/volume_ramp.h b/src/audio_core/renderer/command/mix/volume_ramp.h
new file mode 100644
index 0000000000..77b61547ee
--- /dev/null
+++ b/src/audio_core/renderer/command/mix/volume_ramp.h
@@ -0,0 +1,56 @@
+// 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::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * AudioRenderer command for applying volume to a mix buffer, with ramping for the volume to smooth
+ * out the transition.
+ */
+struct VolumeRampCommand : 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::CommandListProcessor& processor) override;
+
+    /// Fixed point precision
+    u8 precision;
+    /// Input mix buffer index
+    s16 input_index;
+    /// Output mix buffer index
+    s16 output_index;
+    /// Previous mix volume applied to the input
+    f32 prev_volume;
+    /// Current mix volume applied to the input
+    f32 volume;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/performance/performance.cpp b/src/audio_core/renderer/command/performance/performance.cpp
new file mode 100644
index 0000000000..985958b039
--- /dev/null
+++ b/src/audio_core/renderer/command/performance/performance.cpp
@@ -0,0 +1,43 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/adsp/command_list_processor.h"
+#include "audio_core/renderer/command/performance/performance.h"
+#include "core/core.h"
+#include "core/core_timing.h"
+#include "core/core_timing_util.h"
+
+namespace AudioCore::AudioRenderer {
+
+void PerformanceCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor,
+                              std::string& string) {
+    string += fmt::format("PerformanceCommand\n\tstate {}\n", static_cast<u32>(state));
+}
+
+void PerformanceCommand::Process(const ADSP::CommandListProcessor& processor) {
+    auto base{entry_address.translated_address};
+    if (state == PerformanceState::Start) {
+        auto start_time_ptr{reinterpret_cast<u32*>(base + entry_address.entry_start_time_offset)};
+        *start_time_ptr = static_cast<u32>(
+            Core::Timing::CyclesToUs(processor.system->CoreTiming().GetClockTicks() -
+                                     processor.start_time - processor.current_processing_time)
+                .count());
+    } else if (state == PerformanceState::Stop) {
+        auto processed_time_ptr{
+            reinterpret_cast<u32*>(base + entry_address.entry_processed_time_offset)};
+        auto entry_count_ptr{
+            reinterpret_cast<u32*>(base + entry_address.header_entry_count_offset)};
+
+        *processed_time_ptr = static_cast<u32>(
+            Core::Timing::CyclesToUs(processor.system->CoreTiming().GetClockTicks() -
+                                     processor.start_time - processor.current_processing_time)
+                .count());
+        (*entry_count_ptr)++;
+    }
+}
+
+bool PerformanceCommand::Verify(const ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/performance/performance.h b/src/audio_core/renderer/command/performance/performance.h
new file mode 100644
index 0000000000..11a7d6c081
--- /dev/null
+++ b/src/audio_core/renderer/command/performance/performance.h
@@ -0,0 +1,51 @@
+// 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/performance/performance_entry_addresses.h"
+#include "audio_core/renderer/performance/performance_manager.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * AudioRenderer command for writing AudioRenderer performance metrics back to the sysmodule.
+ */
+struct PerformanceCommand : 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::CommandListProcessor& processor) override;
+
+    /// State of the performance
+    PerformanceState state;
+    /// Pointers to be written
+    PerformanceEntryAddresses entry_address;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/resample/downmix_6ch_to_2ch.cpp b/src/audio_core/renderer/command/resample/downmix_6ch_to_2ch.cpp
new file mode 100644
index 0000000000..1fd90308ad
--- /dev/null
+++ b/src/audio_core/renderer/command/resample/downmix_6ch_to_2ch.cpp
@@ -0,0 +1,74 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/adsp/command_list_processor.h"
+#include "audio_core/renderer/command/resample/downmix_6ch_to_2ch.h"
+
+namespace AudioCore::AudioRenderer {
+
+void DownMix6chTo2chCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor,
+                                  std::string& string) {
+    string += fmt::format("DownMix6chTo2chCommand\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 DownMix6chTo2chCommand::Process(const ADSP::CommandListProcessor& processor) {
+    auto in_front_left{
+        processor.mix_buffers.subspan(inputs[0] * processor.sample_count, processor.sample_count)};
+    auto in_front_right{
+        processor.mix_buffers.subspan(inputs[1] * processor.sample_count, processor.sample_count)};
+    auto in_center{
+        processor.mix_buffers.subspan(inputs[2] * processor.sample_count, processor.sample_count)};
+    auto in_lfe{
+        processor.mix_buffers.subspan(inputs[3] * processor.sample_count, processor.sample_count)};
+    auto in_back_left{
+        processor.mix_buffers.subspan(inputs[4] * processor.sample_count, processor.sample_count)};
+    auto in_back_right{
+        processor.mix_buffers.subspan(inputs[5] * processor.sample_count, processor.sample_count)};
+
+    auto out_front_left{
+        processor.mix_buffers.subspan(outputs[0] * processor.sample_count, processor.sample_count)};
+    auto out_front_right{
+        processor.mix_buffers.subspan(outputs[1] * processor.sample_count, processor.sample_count)};
+    auto out_center{
+        processor.mix_buffers.subspan(outputs[2] * processor.sample_count, processor.sample_count)};
+    auto out_lfe{
+        processor.mix_buffers.subspan(outputs[3] * processor.sample_count, processor.sample_count)};
+    auto out_back_left{
+        processor.mix_buffers.subspan(outputs[4] * processor.sample_count, processor.sample_count)};
+    auto out_back_right{
+        processor.mix_buffers.subspan(outputs[5] * processor.sample_count, processor.sample_count)};
+
+    for (u32 i = 0; i < processor.sample_count; i++) {
+        const auto left_sample{(in_front_left[i] * down_mix_coeff[0] +
+                                in_center[i] * down_mix_coeff[1] + in_lfe[i] * down_mix_coeff[2] +
+                                in_back_left[i] * down_mix_coeff[3])
+                                   .to_int()};
+
+        const auto right_sample{(in_front_right[i] * down_mix_coeff[0] +
+                                 in_center[i] * down_mix_coeff[1] + in_lfe[i] * down_mix_coeff[2] +
+                                 in_back_right[i] * down_mix_coeff[3])
+                                    .to_int()};
+
+        out_front_left[i] = left_sample;
+        out_front_right[i] = right_sample;
+    }
+
+    std::memset(out_center.data(), 0, out_center.size_bytes());
+    std::memset(out_lfe.data(), 0, out_lfe.size_bytes());
+    std::memset(out_back_left.data(), 0, out_back_left.size_bytes());
+    std::memset(out_back_right.data(), 0, out_back_right.size_bytes());
+}
+
+bool DownMix6chTo2chCommand::Verify(const ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/resample/downmix_6ch_to_2ch.h b/src/audio_core/renderer/command/resample/downmix_6ch_to_2ch.h
new file mode 100644
index 0000000000..dc133a73be
--- /dev/null
+++ b/src/audio_core/renderer/command/resample/downmix_6ch_to_2ch.h
@@ -0,0 +1,59 @@
+// 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"
+#include "common/fixed_point.h"
+
+namespace AudioCore::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * AudioRenderer command for downmixing 6 channels to 2.
+ * Channel layout (SMPTE):
+ *     0 - front left
+ *     1 - front right
+ *     2 - center
+ *     3 - lfe
+ *     4 - back left
+ *     5 - back right
+ */
+struct DownMix6chTo2chCommand : 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::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;
+    /// Coefficients used for downmixing
+    std::array<Common::FixedPoint<48, 16>, 4> down_mix_coeff;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/resample/resample.cpp b/src/audio_core/renderer/command/resample/resample.cpp
new file mode 100644
index 0000000000..070c9d2b88
--- /dev/null
+++ b/src/audio_core/renderer/command/resample/resample.cpp
@@ -0,0 +1,883 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/command/resample/resample.h"
+
+namespace AudioCore::AudioRenderer {
+
+static void ResampleLowQuality(std::span<s32> output, std::span<const s16> input,
+                               const Common::FixedPoint<49, 15>& sample_rate_ratio,
+                               Common::FixedPoint<49, 15>& fraction, const u32 samples_to_write) {
+    if (sample_rate_ratio == 1.0f) {
+        for (u32 i = 0; i < samples_to_write; i++) {
+            output[i] = input[i];
+        }
+    } else {
+        u32 read_index{0};
+        for (u32 i = 0; i < samples_to_write; i++) {
+            output[i] = input[read_index + (fraction >= 0.5f)];
+            fraction += sample_rate_ratio;
+            read_index += static_cast<u32>(fraction.to_int_floor());
+            fraction.clear_int();
+        }
+    }
+}
+
+static void ResampleNormalQuality(std::span<s32> output, std::span<const s16> input,
+                                  const Common::FixedPoint<49, 15>& sample_rate_ratio,
+                                  Common::FixedPoint<49, 15>& fraction,
+                                  const u32 samples_to_write) {
+    static constexpr std::array<f32, 512> lut0 = {
+        0.20141602f, 0.59283447f, 0.20513916f, 0.00009155f, 0.19772339f, 0.59277344f, 0.20889282f,
+        0.00027466f, 0.19406128f, 0.59262085f, 0.21264648f, 0.00045776f, 0.19039917f, 0.59240723f,
+        0.21646118f, 0.00067139f, 0.18679810f, 0.59213257f, 0.22030640f, 0.00085449f, 0.18322754f,
+        0.59176636f, 0.22415161f, 0.00103760f, 0.17968750f, 0.59133911f, 0.22802734f, 0.00125122f,
+        0.17617798f, 0.59085083f, 0.23193359f, 0.00146484f, 0.17269897f, 0.59027100f, 0.23583984f,
+        0.00167847f, 0.16925049f, 0.58963013f, 0.23977661f, 0.00189209f, 0.16583252f, 0.58892822f,
+        0.24374390f, 0.00210571f, 0.16244507f, 0.58816528f, 0.24774170f, 0.00234985f, 0.15908813f,
+        0.58731079f, 0.25173950f, 0.00256348f, 0.15576172f, 0.58639526f, 0.25576782f, 0.00280762f,
+        0.15249634f, 0.58541870f, 0.25979614f, 0.00308228f, 0.14923096f, 0.58435059f, 0.26385498f,
+        0.00332642f, 0.14602661f, 0.58325195f, 0.26794434f, 0.00360107f, 0.14285278f, 0.58206177f,
+        0.27203369f, 0.00387573f, 0.13973999f, 0.58078003f, 0.27612305f, 0.00418091f, 0.13662720f,
+        0.57946777f, 0.28024292f, 0.00448608f, 0.13357544f, 0.57806396f, 0.28436279f, 0.00479126f,
+        0.13052368f, 0.57662964f, 0.28851318f, 0.00512695f, 0.12753296f, 0.57510376f, 0.29266357f,
+        0.00546265f, 0.12460327f, 0.57351685f, 0.29681396f, 0.00579834f, 0.12167358f, 0.57183838f,
+        0.30099487f, 0.00616455f, 0.11880493f, 0.57012939f, 0.30517578f, 0.00656128f, 0.11596680f,
+        0.56835938f, 0.30935669f, 0.00695801f, 0.11318970f, 0.56649780f, 0.31353760f, 0.00735474f,
+        0.11041260f, 0.56457520f, 0.31771851f, 0.00778198f, 0.10769653f, 0.56262207f, 0.32192993f,
+        0.00823975f, 0.10501099f, 0.56057739f, 0.32614136f, 0.00869751f, 0.10238647f, 0.55847168f,
+        0.33032227f, 0.00915527f, 0.09976196f, 0.55633545f, 0.33453369f, 0.00967407f, 0.09722900f,
+        0.55410767f, 0.33874512f, 0.01019287f, 0.09469604f, 0.55181885f, 0.34295654f, 0.01071167f,
+        0.09222412f, 0.54949951f, 0.34713745f, 0.01126099f, 0.08978271f, 0.54708862f, 0.35134888f,
+        0.01184082f, 0.08737183f, 0.54464722f, 0.35552979f, 0.01245117f, 0.08499146f, 0.54214478f,
+        0.35974121f, 0.01306152f, 0.08267212f, 0.53958130f, 0.36392212f, 0.01370239f, 0.08041382f,
+        0.53695679f, 0.36810303f, 0.01437378f, 0.07815552f, 0.53427124f, 0.37225342f, 0.01507568f,
+        0.07595825f, 0.53155518f, 0.37640381f, 0.01577759f, 0.07379150f, 0.52877808f, 0.38055420f,
+        0.01651001f, 0.07165527f, 0.52593994f, 0.38470459f, 0.01727295f, 0.06958008f, 0.52307129f,
+        0.38882446f, 0.01806641f, 0.06753540f, 0.52014160f, 0.39294434f, 0.01889038f, 0.06552124f,
+        0.51715088f, 0.39703369f, 0.01974487f, 0.06356812f, 0.51409912f, 0.40112305f, 0.02059937f,
+        0.06164551f, 0.51101685f, 0.40518188f, 0.02148438f, 0.05975342f, 0.50790405f, 0.40921021f,
+        0.02243042f, 0.05789185f, 0.50473022f, 0.41323853f, 0.02337646f, 0.05609131f, 0.50152588f,
+        0.41726685f, 0.02435303f, 0.05432129f, 0.49826050f, 0.42123413f, 0.02539062f, 0.05258179f,
+        0.49493408f, 0.42520142f, 0.02642822f, 0.05087280f, 0.49160767f, 0.42913818f, 0.02749634f,
+        0.04922485f, 0.48822021f, 0.43307495f, 0.02859497f, 0.04760742f, 0.48477173f, 0.43695068f,
+        0.02975464f, 0.04602051f, 0.48132324f, 0.44082642f, 0.03091431f, 0.04446411f, 0.47781372f,
+        0.44467163f, 0.03210449f, 0.04293823f, 0.47424316f, 0.44845581f, 0.03335571f, 0.04147339f,
+        0.47067261f, 0.45223999f, 0.03460693f, 0.04003906f, 0.46704102f, 0.45599365f, 0.03591919f,
+        0.03863525f, 0.46340942f, 0.45971680f, 0.03726196f, 0.03726196f, 0.45971680f, 0.46340942f,
+        0.03863525f, 0.03591919f, 0.45599365f, 0.46704102f, 0.04003906f, 0.03460693f, 0.45223999f,
+        0.47067261f, 0.04147339f, 0.03335571f, 0.44845581f, 0.47424316f, 0.04293823f, 0.03210449f,
+        0.44467163f, 0.47781372f, 0.04446411f, 0.03091431f, 0.44082642f, 0.48132324f, 0.04602051f,
+        0.02975464f, 0.43695068f, 0.48477173f, 0.04760742f, 0.02859497f, 0.43307495f, 0.48822021f,
+        0.04922485f, 0.02749634f, 0.42913818f, 0.49160767f, 0.05087280f, 0.02642822f, 0.42520142f,
+        0.49493408f, 0.05258179f, 0.02539062f, 0.42123413f, 0.49826050f, 0.05432129f, 0.02435303f,
+        0.41726685f, 0.50152588f, 0.05609131f, 0.02337646f, 0.41323853f, 0.50473022f, 0.05789185f,
+        0.02243042f, 0.40921021f, 0.50790405f, 0.05975342f, 0.02148438f, 0.40518188f, 0.51101685f,
+        0.06164551f, 0.02059937f, 0.40112305f, 0.51409912f, 0.06356812f, 0.01974487f, 0.39703369f,
+        0.51715088f, 0.06552124f, 0.01889038f, 0.39294434f, 0.52014160f, 0.06753540f, 0.01806641f,
+        0.38882446f, 0.52307129f, 0.06958008f, 0.01727295f, 0.38470459f, 0.52593994f, 0.07165527f,
+        0.01651001f, 0.38055420f, 0.52877808f, 0.07379150f, 0.01577759f, 0.37640381f, 0.53155518f,
+        0.07595825f, 0.01507568f, 0.37225342f, 0.53427124f, 0.07815552f, 0.01437378f, 0.36810303f,
+        0.53695679f, 0.08041382f, 0.01370239f, 0.36392212f, 0.53958130f, 0.08267212f, 0.01306152f,
+        0.35974121f, 0.54214478f, 0.08499146f, 0.01245117f, 0.35552979f, 0.54464722f, 0.08737183f,
+        0.01184082f, 0.35134888f, 0.54708862f, 0.08978271f, 0.01126099f, 0.34713745f, 0.54949951f,
+        0.09222412f, 0.01071167f, 0.34295654f, 0.55181885f, 0.09469604f, 0.01019287f, 0.33874512f,
+        0.55410767f, 0.09722900f, 0.00967407f, 0.33453369f, 0.55633545f, 0.09976196f, 0.00915527f,
+        0.33032227f, 0.55847168f, 0.10238647f, 0.00869751f, 0.32614136f, 0.56057739f, 0.10501099f,
+        0.00823975f, 0.32192993f, 0.56262207f, 0.10769653f, 0.00778198f, 0.31771851f, 0.56457520f,
+        0.11041260f, 0.00735474f, 0.31353760f, 0.56649780f, 0.11318970f, 0.00695801f, 0.30935669f,
+        0.56835938f, 0.11596680f, 0.00656128f, 0.30517578f, 0.57012939f, 0.11880493f, 0.00616455f,
+        0.30099487f, 0.57183838f, 0.12167358f, 0.00579834f, 0.29681396f, 0.57351685f, 0.12460327f,
+        0.00546265f, 0.29266357f, 0.57510376f, 0.12753296f, 0.00512695f, 0.28851318f, 0.57662964f,
+        0.13052368f, 0.00479126f, 0.28436279f, 0.57806396f, 0.13357544f, 0.00448608f, 0.28024292f,
+        0.57946777f, 0.13662720f, 0.00418091f, 0.27612305f, 0.58078003f, 0.13973999f, 0.00387573f,
+        0.27203369f, 0.58206177f, 0.14285278f, 0.00360107f, 0.26794434f, 0.58325195f, 0.14602661f,
+        0.00332642f, 0.26385498f, 0.58435059f, 0.14923096f, 0.00308228f, 0.25979614f, 0.58541870f,
+        0.15249634f, 0.00280762f, 0.25576782f, 0.58639526f, 0.15576172f, 0.00256348f, 0.25173950f,
+        0.58731079f, 0.15908813f, 0.00234985f, 0.24774170f, 0.58816528f, 0.16244507f, 0.00210571f,
+        0.24374390f, 0.58892822f, 0.16583252f, 0.00189209f, 0.23977661f, 0.58963013f, 0.16925049f,
+        0.00167847f, 0.23583984f, 0.59027100f, 0.17269897f, 0.00146484f, 0.23193359f, 0.59085083f,
+        0.17617798f, 0.00125122f, 0.22802734f, 0.59133911f, 0.17968750f, 0.00103760f, 0.22415161f,
+        0.59176636f, 0.18322754f, 0.00085449f, 0.22030640f, 0.59213257f, 0.18679810f, 0.00067139f,
+        0.21646118f, 0.59240723f, 0.19039917f, 0.00045776f, 0.21264648f, 0.59262085f, 0.19406128f,
+        0.00027466f, 0.20889282f, 0.59277344f, 0.19772339f, 0.00009155f, 0.20513916f, 0.59283447f,
+        0.20141602f,
+    };
+
+    static constexpr std::array<f32, 512> lut1 = {
+        0.00207520f,  0.99606323f,  0.00210571f,  -0.00015259f, -0.00610352f, 0.99578857f,
+        0.00646973f,  -0.00045776f, -0.01000977f, 0.99526978f,  0.01095581f,  -0.00079346f,
+        -0.01373291f, 0.99444580f,  0.01562500f,  -0.00109863f, -0.01733398f, 0.99337769f,
+        0.02041626f,  -0.00143433f, -0.02075195f, 0.99203491f,  0.02539062f,  -0.00177002f,
+        -0.02404785f, 0.99041748f,  0.03051758f,  -0.00210571f, -0.02719116f, 0.98855591f,
+        0.03582764f,  -0.00244141f, -0.03021240f, 0.98641968f,  0.04125977f,  -0.00280762f,
+        -0.03308105f, 0.98400879f,  0.04687500f,  -0.00314331f, -0.03579712f, 0.98135376f,
+        0.05261230f,  -0.00350952f, -0.03839111f, 0.97842407f,  0.05856323f,  -0.00390625f,
+        -0.04083252f, 0.97521973f,  0.06463623f,  -0.00427246f, -0.04315186f, 0.97180176f,
+        0.07086182f,  -0.00466919f, -0.04534912f, 0.96810913f,  0.07727051f,  -0.00509644f,
+        -0.04742432f, 0.96414185f,  0.08383179f,  -0.00549316f, -0.04934692f, 0.95996094f,
+        0.09054565f,  -0.00592041f, -0.05114746f, 0.95550537f,  0.09741211f,  -0.00637817f,
+        -0.05285645f, 0.95083618f,  0.10443115f,  -0.00683594f, -0.05441284f, 0.94589233f,
+        0.11160278f,  -0.00732422f, -0.05584717f, 0.94073486f,  0.11892700f,  -0.00781250f,
+        -0.05718994f, 0.93533325f,  0.12643433f,  -0.00830078f, -0.05841064f, 0.92968750f,
+        0.13406372f,  -0.00881958f, -0.05953979f, 0.92382812f,  0.14184570f,  -0.00936890f,
+        -0.06054688f, 0.91772461f,  0.14978027f,  -0.00991821f, -0.06146240f, 0.91143799f,
+        0.15783691f,  -0.01046753f, -0.06225586f, 0.90490723f,  0.16607666f,  -0.01104736f,
+        -0.06295776f, 0.89816284f,  0.17443848f,  -0.01165771f, -0.06356812f, 0.89120483f,
+        0.18292236f,  -0.01229858f, -0.06408691f, 0.88403320f,  0.19155884f,  -0.01293945f,
+        -0.06451416f, 0.87667847f,  0.20034790f,  -0.01358032f, -0.06484985f, 0.86914062f,
+        0.20925903f,  -0.01428223f, -0.06509399f, 0.86138916f,  0.21829224f,  -0.01495361f,
+        -0.06527710f, 0.85345459f,  0.22744751f,  -0.01568604f, -0.06536865f, 0.84533691f,
+        0.23675537f,  -0.01641846f, -0.06536865f, 0.83703613f,  0.24615479f,  -0.01718140f,
+        -0.06533813f, 0.82858276f,  0.25567627f,  -0.01794434f, -0.06518555f, 0.81991577f,
+        0.26531982f,  -0.01873779f, -0.06500244f, 0.81112671f,  0.27505493f,  -0.01956177f,
+        -0.06472778f, 0.80215454f,  0.28491211f,  -0.02038574f, -0.06442261f, 0.79306030f,
+        0.29489136f,  -0.02124023f, -0.06402588f, 0.78378296f,  0.30496216f,  -0.02209473f,
+        -0.06359863f, 0.77438354f,  0.31512451f,  -0.02297974f, -0.06307983f, 0.76486206f,
+        0.32537842f,  -0.02389526f, -0.06253052f, 0.75518799f,  0.33569336f,  -0.02481079f,
+        -0.06195068f, 0.74539185f,  0.34613037f,  -0.02575684f, -0.06130981f, 0.73547363f,
+        0.35662842f,  -0.02670288f, -0.06060791f, 0.72543335f,  0.36721802f,  -0.02767944f,
+        -0.05987549f, 0.71527100f,  0.37786865f,  -0.02865601f, -0.05911255f, 0.70504761f,
+        0.38858032f,  -0.02966309f, -0.05831909f, 0.69470215f,  0.39935303f,  -0.03067017f,
+        -0.05746460f, 0.68426514f,  0.41018677f,  -0.03170776f, -0.05661011f, 0.67373657f,
+        0.42108154f,  -0.03271484f, -0.05569458f, 0.66311646f,  0.43200684f,  -0.03378296f,
+        -0.05477905f, 0.65246582f,  0.44299316f,  -0.03482056f, -0.05383301f, 0.64169312f,
+        0.45401001f,  -0.03588867f, -0.05285645f, 0.63088989f,  0.46505737f,  -0.03695679f,
+        -0.05187988f, 0.62002563f,  0.47613525f,  -0.03802490f, -0.05087280f, 0.60910034f,
+        0.48721313f,  -0.03912354f, -0.04983521f, 0.59814453f,  0.49832153f,  -0.04019165f,
+        -0.04879761f, 0.58712769f,  0.50946045f,  -0.04129028f, -0.04772949f, 0.57611084f,
+        0.52056885f,  -0.04235840f, -0.04669189f, 0.56503296f,  0.53170776f,  -0.04345703f,
+        -0.04562378f, 0.55392456f,  0.54281616f,  -0.04452515f, -0.04452515f, 0.54281616f,
+        0.55392456f,  -0.04562378f, -0.04345703f, 0.53170776f,  0.56503296f,  -0.04669189f,
+        -0.04235840f, 0.52056885f,  0.57611084f,  -0.04772949f, -0.04129028f, 0.50946045f,
+        0.58712769f,  -0.04879761f, -0.04019165f, 0.49832153f,  0.59814453f,  -0.04983521f,
+        -0.03912354f, 0.48721313f,  0.60910034f,  -0.05087280f, -0.03802490f, 0.47613525f,
+        0.62002563f,  -0.05187988f, -0.03695679f, 0.46505737f,  0.63088989f,  -0.05285645f,
+        -0.03588867f, 0.45401001f,  0.64169312f,  -0.05383301f, -0.03482056f, 0.44299316f,
+        0.65246582f,  -0.05477905f, -0.03378296f, 0.43200684f,  0.66311646f,  -0.05569458f,
+        -0.03271484f, 0.42108154f,  0.67373657f,  -0.05661011f, -0.03170776f, 0.41018677f,
+        0.68426514f,  -0.05746460f, -0.03067017f, 0.39935303f,  0.69470215f,  -0.05831909f,
+        -0.02966309f, 0.38858032f,  0.70504761f,  -0.05911255f, -0.02865601f, 0.37786865f,
+        0.71527100f,  -0.05987549f, -0.02767944f, 0.36721802f,  0.72543335f,  -0.06060791f,
+        -0.02670288f, 0.35662842f,  0.73547363f,  -0.06130981f, -0.02575684f, 0.34613037f,
+        0.74539185f,  -0.06195068f, -0.02481079f, 0.33569336f,  0.75518799f,  -0.06253052f,
+        -0.02389526f, 0.32537842f,  0.76486206f,  -0.06307983f, -0.02297974f, 0.31512451f,
+        0.77438354f,  -0.06359863f, -0.02209473f, 0.30496216f,  0.78378296f,  -0.06402588f,
+        -0.02124023f, 0.29489136f,  0.79306030f,  -0.06442261f, -0.02038574f, 0.28491211f,
+        0.80215454f,  -0.06472778f, -0.01956177f, 0.27505493f,  0.81112671f,  -0.06500244f,
+        -0.01873779f, 0.26531982f,  0.81991577f,  -0.06518555f, -0.01794434f, 0.25567627f,
+        0.82858276f,  -0.06533813f, -0.01718140f, 0.24615479f,  0.83703613f,  -0.06536865f,
+        -0.01641846f, 0.23675537f,  0.84533691f,  -0.06536865f, -0.01568604f, 0.22744751f,
+        0.85345459f,  -0.06527710f, -0.01495361f, 0.21829224f,  0.86138916f,  -0.06509399f,
+        -0.01428223f, 0.20925903f,  0.86914062f,  -0.06484985f, -0.01358032f, 0.20034790f,
+        0.87667847f,  -0.06451416f, -0.01293945f, 0.19155884f,  0.88403320f,  -0.06408691f,
+        -0.01229858f, 0.18292236f,  0.89120483f,  -0.06356812f, -0.01165771f, 0.17443848f,
+        0.89816284f,  -0.06295776f, -0.01104736f, 0.16607666f,  0.90490723f,  -0.06225586f,
+        -0.01046753f, 0.15783691f,  0.91143799f,  -0.06146240f, -0.00991821f, 0.14978027f,
+        0.91772461f,  -0.06054688f, -0.00936890f, 0.14184570f,  0.92382812f,  -0.05953979f,
+        -0.00881958f, 0.13406372f,  0.92968750f,  -0.05841064f, -0.00830078f, 0.12643433f,
+        0.93533325f,  -0.05718994f, -0.00781250f, 0.11892700f,  0.94073486f,  -0.05584717f,
+        -0.00732422f, 0.11160278f,  0.94589233f,  -0.05441284f, -0.00683594f, 0.10443115f,
+        0.95083618f,  -0.05285645f, -0.00637817f, 0.09741211f,  0.95550537f,  -0.05114746f,
+        -0.00592041f, 0.09054565f,  0.95996094f,  -0.04934692f, -0.00549316f, 0.08383179f,
+        0.96414185f,  -0.04742432f, -0.00509644f, 0.07727051f,  0.96810913f,  -0.04534912f,
+        -0.00466919f, 0.07086182f,  0.97180176f,  -0.04315186f, -0.00427246f, 0.06463623f,
+        0.97521973f,  -0.04083252f, -0.00390625f, 0.05856323f,  0.97842407f,  -0.03839111f,
+        -0.00350952f, 0.05261230f,  0.98135376f,  -0.03579712f, -0.00314331f, 0.04687500f,
+        0.98400879f,  -0.03308105f, -0.00280762f, 0.04125977f,  0.98641968f,  -0.03021240f,
+        -0.00244141f, 0.03582764f,  0.98855591f,  -0.02719116f, -0.00210571f, 0.03051758f,
+        0.99041748f,  -0.02404785f, -0.00177002f, 0.02539062f,  0.99203491f,  -0.02075195f,
+        -0.00143433f, 0.02041626f,  0.99337769f,  -0.01733398f, -0.00109863f, 0.01562500f,
+        0.99444580f,  -0.01373291f, -0.00079346f, 0.01095581f,  0.99526978f,  -0.01000977f,
+        -0.00045776f, 0.00646973f,  0.99578857f,  -0.00610352f, -0.00015259f, 0.00210571f,
+        0.99606323f,  -0.00207520f,
+    };
+
+    static constexpr std::array<f32, 512> lut2 = {
+        0.09750366f,  0.80221558f,  0.10159302f,  -0.00097656f, 0.09350586f,  0.80203247f,
+        0.10580444f,  -0.00103760f, 0.08959961f,  0.80169678f,  0.11010742f,  -0.00115967f,
+        0.08578491f,  0.80117798f,  0.11447144f,  -0.00128174f, 0.08203125f,  0.80047607f,
+        0.11892700f,  -0.00140381f, 0.07836914f,  0.79962158f,  0.12347412f,  -0.00152588f,
+        0.07479858f,  0.79861450f,  0.12814331f,  -0.00164795f, 0.07135010f,  0.79742432f,
+        0.13287354f,  -0.00177002f, 0.06796265f,  0.79605103f,  0.13769531f,  -0.00192261f,
+        0.06469727f,  0.79452515f,  0.14260864f,  -0.00204468f, 0.06149292f,  0.79284668f,
+        0.14761353f,  -0.00219727f, 0.05834961f,  0.79098511f,  0.15270996f,  -0.00231934f,
+        0.05532837f,  0.78894043f,  0.15789795f,  -0.00247192f, 0.05236816f,  0.78674316f,
+        0.16317749f,  -0.00265503f, 0.04949951f,  0.78442383f,  0.16851807f,  -0.00280762f,
+        0.04672241f,  0.78189087f,  0.17398071f,  -0.00299072f, 0.04400635f,  0.77920532f,
+        0.17950439f,  -0.00314331f, 0.04141235f,  0.77636719f,  0.18511963f,  -0.00332642f,
+        0.03887939f,  0.77337646f,  0.19082642f,  -0.00350952f, 0.03640747f,  0.77023315f,
+        0.19659424f,  -0.00369263f, 0.03402710f,  0.76693726f,  0.20248413f,  -0.00387573f,
+        0.03173828f,  0.76348877f,  0.20843506f,  -0.00405884f, 0.02951050f,  0.75985718f,
+        0.21444702f,  -0.00427246f, 0.02737427f,  0.75610352f,  0.22055054f,  -0.00445557f,
+        0.02529907f,  0.75219727f,  0.22674561f,  -0.00466919f, 0.02331543f,  0.74816895f,
+        0.23300171f,  -0.00485229f, 0.02139282f,  0.74398804f,  0.23931885f,  -0.00506592f,
+        0.01956177f,  0.73965454f,  0.24572754f,  -0.00531006f, 0.01779175f,  0.73519897f,
+        0.25219727f,  -0.00552368f, 0.01605225f,  0.73059082f,  0.25872803f,  -0.00570679f,
+        0.01440430f,  0.72586060f,  0.26535034f,  -0.00592041f, 0.01281738f,  0.72100830f,
+        0.27203369f,  -0.00616455f, 0.01132202f,  0.71600342f,  0.27877808f,  -0.00637817f,
+        0.00988770f,  0.71090698f,  0.28558350f,  -0.00656128f, 0.00851440f,  0.70565796f,
+        0.29244995f,  -0.00677490f, 0.00720215f,  0.70031738f,  0.29934692f,  -0.00701904f,
+        0.00592041f,  0.69485474f,  0.30633545f,  -0.00723267f, 0.00469971f,  0.68927002f,
+        0.31338501f,  -0.00741577f, 0.00357056f,  0.68356323f,  0.32046509f,  -0.00762939f,
+        0.00247192f,  0.67773438f,  0.32760620f,  -0.00787354f, 0.00143433f,  0.67184448f,
+        0.33477783f,  -0.00808716f, 0.00045776f,  0.66583252f,  0.34197998f,  -0.00827026f,
+        -0.00048828f, 0.65972900f,  0.34924316f,  -0.00845337f, -0.00134277f, 0.65353394f,
+        0.35656738f,  -0.00863647f, -0.00216675f, 0.64721680f,  0.36389160f,  -0.00885010f,
+        -0.00296021f, 0.64083862f,  0.37127686f,  -0.00903320f, -0.00369263f, 0.63433838f,
+        0.37869263f,  -0.00921631f, -0.00436401f, 0.62777710f,  0.38613892f,  -0.00933838f,
+        -0.00497437f, 0.62115479f,  0.39361572f,  -0.00949097f, -0.00558472f, 0.61444092f,
+        0.40109253f,  -0.00964355f, -0.00613403f, 0.60763550f,  0.40859985f,  -0.00979614f,
+        -0.00665283f, 0.60076904f,  0.41610718f,  -0.00991821f, -0.00714111f, 0.59384155f,
+        0.42364502f,  -0.01000977f, -0.00756836f, 0.58685303f,  0.43121338f,  -0.01013184f,
+        -0.00796509f, 0.57977295f,  0.43875122f,  -0.01022339f, -0.00833130f, 0.57266235f,
+        0.44631958f,  -0.01028442f, -0.00866699f, 0.56552124f,  0.45388794f,  -0.01034546f,
+        -0.00897217f, 0.55831909f,  0.46145630f,  -0.01040649f, -0.00921631f, 0.55105591f,
+        0.46902466f,  -0.01040649f, -0.00946045f, 0.54373169f,  0.47659302f,  -0.01040649f,
+        -0.00967407f, 0.53640747f,  0.48413086f,  -0.01037598f, -0.00985718f, 0.52902222f,
+        0.49166870f,  -0.01037598f, -0.01000977f, 0.52160645f,  0.49917603f,  -0.01031494f,
+        -0.01013184f, 0.51416016f,  0.50668335f,  -0.01025391f, -0.01025391f, 0.50668335f,
+        0.51416016f,  -0.01013184f, -0.01031494f, 0.49917603f,  0.52160645f,  -0.01000977f,
+        -0.01037598f, 0.49166870f,  0.52902222f,  -0.00985718f, -0.01037598f, 0.48413086f,
+        0.53640747f,  -0.00967407f, -0.01040649f, 0.47659302f,  0.54373169f,  -0.00946045f,
+        -0.01040649f, 0.46902466f,  0.55105591f,  -0.00921631f, -0.01040649f, 0.46145630f,
+        0.55831909f,  -0.00897217f, -0.01034546f, 0.45388794f,  0.56552124f,  -0.00866699f,
+        -0.01028442f, 0.44631958f,  0.57266235f,  -0.00833130f, -0.01022339f, 0.43875122f,
+        0.57977295f,  -0.00796509f, -0.01013184f, 0.43121338f,  0.58685303f,  -0.00756836f,
+        -0.01000977f, 0.42364502f,  0.59384155f,  -0.00714111f, -0.00991821f, 0.41610718f,
+        0.60076904f,  -0.00665283f, -0.00979614f, 0.40859985f,  0.60763550f,  -0.00613403f,
+        -0.00964355f, 0.40109253f,  0.61444092f,  -0.00558472f, -0.00949097f, 0.39361572f,
+        0.62115479f,  -0.00497437f, -0.00933838f, 0.38613892f,  0.62777710f,  -0.00436401f,
+        -0.00921631f, 0.37869263f,  0.63433838f,  -0.00369263f, -0.00903320f, 0.37127686f,
+        0.64083862f,  -0.00296021f, -0.00885010f, 0.36389160f,  0.64721680f,  -0.00216675f,
+        -0.00863647f, 0.35656738f,  0.65353394f,  -0.00134277f, -0.00845337f, 0.34924316f,
+        0.65972900f,  -0.00048828f, -0.00827026f, 0.34197998f,  0.66583252f,  0.00045776f,
+        -0.00808716f, 0.33477783f,  0.67184448f,  0.00143433f,  -0.00787354f, 0.32760620f,
+        0.67773438f,  0.00247192f,  -0.00762939f, 0.32046509f,  0.68356323f,  0.00357056f,
+        -0.00741577f, 0.31338501f,  0.68927002f,  0.00469971f,  -0.00723267f, 0.30633545f,
+        0.69485474f,  0.00592041f,  -0.00701904f, 0.29934692f,  0.70031738f,  0.00720215f,
+        -0.00677490f, 0.29244995f,  0.70565796f,  0.00851440f,  -0.00656128f, 0.28558350f,
+        0.71090698f,  0.00988770f,  -0.00637817f, 0.27877808f,  0.71600342f,  0.01132202f,
+        -0.00616455f, 0.27203369f,  0.72100830f,  0.01281738f,  -0.00592041f, 0.26535034f,
+        0.72586060f,  0.01440430f,  -0.00570679f, 0.25872803f,  0.73059082f,  0.01605225f,
+        -0.00552368f, 0.25219727f,  0.73519897f,  0.01779175f,  -0.00531006f, 0.24572754f,
+        0.73965454f,  0.01956177f,  -0.00506592f, 0.23931885f,  0.74398804f,  0.02139282f,
+        -0.00485229f, 0.23300171f,  0.74816895f,  0.02331543f,  -0.00466919f, 0.22674561f,
+        0.75219727f,  0.02529907f,  -0.00445557f, 0.22055054f,  0.75610352f,  0.02737427f,
+        -0.00427246f, 0.21444702f,  0.75985718f,  0.02951050f,  -0.00405884f, 0.20843506f,
+        0.76348877f,  0.03173828f,  -0.00387573f, 0.20248413f,  0.76693726f,  0.03402710f,
+        -0.00369263f, 0.19659424f,  0.77023315f,  0.03640747f,  -0.00350952f, 0.19082642f,
+        0.77337646f,  0.03887939f,  -0.00332642f, 0.18511963f,  0.77636719f,  0.04141235f,
+        -0.00314331f, 0.17950439f,  0.77920532f,  0.04400635f,  -0.00299072f, 0.17398071f,
+        0.78189087f,  0.04672241f,  -0.00280762f, 0.16851807f,  0.78442383f,  0.04949951f,
+        -0.00265503f, 0.16317749f,  0.78674316f,  0.05236816f,  -0.00247192f, 0.15789795f,
+        0.78894043f,  0.05532837f,  -0.00231934f, 0.15270996f,  0.79098511f,  0.05834961f,
+        -0.00219727f, 0.14761353f,  0.79284668f,  0.06149292f,  -0.00204468f, 0.14260864f,
+        0.79452515f,  0.06469727f,  -0.00192261f, 0.13769531f,  0.79605103f,  0.06796265f,
+        -0.00177002f, 0.13287354f,  0.79742432f,  0.07135010f,  -0.00164795f, 0.12814331f,
+        0.79861450f,  0.07479858f,  -0.00152588f, 0.12347412f,  0.79962158f,  0.07836914f,
+        -0.00140381f, 0.11892700f,  0.80047607f,  0.08203125f,  -0.00128174f, 0.11447144f,
+        0.80117798f,  0.08578491f,  -0.00115967f, 0.11010742f,  0.80169678f,  0.08959961f,
+        -0.00103760f, 0.10580444f,  0.80203247f,  0.09350586f,  -0.00097656f, 0.10159302f,
+        0.80221558f,  0.09750366f,
+    };
+
+    const auto get_lut = [&]() -> std::span<const f32> {
+        if (sample_rate_ratio <= 1.0f) {
+            return std::span<const f32>(lut2.data(), lut2.size());
+        } else if (sample_rate_ratio < 1.3f) {
+            return std::span<const f32>(lut1.data(), lut1.size());
+        } else {
+            return std::span<const f32>(lut0.data(), lut0.size());
+        }
+    };
+
+    auto lut{get_lut()};
+    u32 read_index{0};
+    for (u32 i = 0; i < samples_to_write; i++) {
+        const auto lut_index{(fraction.get_frac() >> 8) * 4};
+        const Common::FixedPoint<56, 8> sample0{input[read_index + 0] * lut[lut_index + 0]};
+        const Common::FixedPoint<56, 8> sample1{input[read_index + 1] * lut[lut_index + 1]};
+        const Common::FixedPoint<56, 8> sample2{input[read_index + 2] * lut[lut_index + 2]};
+        const Common::FixedPoint<56, 8> sample3{input[read_index + 3] * lut[lut_index + 3]};
+        output[i] = (sample0 + sample1 + sample2 + sample3).to_int_floor();
+        fraction += sample_rate_ratio;
+        read_index += static_cast<u32>(fraction.to_int_floor());
+        fraction.clear_int();
+    }
+}
+
+static void ResampleHighQuality(std::span<s32> output, std::span<const s16> input,
+                                const Common::FixedPoint<49, 15>& sample_rate_ratio,
+                                Common::FixedPoint<49, 15>& fraction, const u32 samples_to_write) {
+    static constexpr std::array<f32, 1024> lut0 = {
+        -0.01776123f, -0.00070190f, 0.26672363f,  0.50006104f,  0.26956177f,  0.00024414f,
+        -0.01800537f, 0.00000000f,  -0.01748657f, -0.00164795f, 0.26388550f,  0.50003052f,
+        0.27236938f,  0.00122070f,  -0.01824951f, -0.00003052f, -0.01724243f, -0.00256348f,
+        0.26107788f,  0.49996948f,  0.27520752f,  0.00219727f,  -0.01849365f, -0.00003052f,
+        -0.01699829f, -0.00344849f, 0.25823975f,  0.49984741f,  0.27801514f,  0.00320435f,
+        -0.01873779f, -0.00006104f, -0.01675415f, -0.00433350f, 0.25543213f,  0.49972534f,
+        0.28085327f,  0.00424194f,  -0.01898193f, -0.00006104f, -0.01651001f, -0.00518799f,
+        0.25259399f,  0.49954224f,  0.28366089f,  0.00527954f,  -0.01922607f, -0.00009155f,
+        -0.01626587f, -0.00604248f, 0.24978638f,  0.49932861f,  0.28646851f,  0.00634766f,
+        -0.01947021f, -0.00012207f, -0.01602173f, -0.00686646f, 0.24697876f,  0.49908447f,
+        0.28930664f,  0.00744629f,  -0.01971436f, -0.00015259f, -0.01574707f, -0.00765991f,
+        0.24414062f,  0.49877930f,  0.29211426f,  0.00854492f,  -0.01995850f, -0.00015259f,
+        -0.01550293f, -0.00845337f, 0.24133301f,  0.49847412f,  0.29492188f,  0.00967407f,
+        -0.02020264f, -0.00018311f, -0.01525879f, -0.00921631f, 0.23852539f,  0.49810791f,
+        0.29772949f,  0.01083374f,  -0.02044678f, -0.00021362f, -0.01501465f, -0.00997925f,
+        0.23571777f,  0.49774170f,  0.30050659f,  0.01199341f,  -0.02069092f, -0.00024414f,
+        -0.01477051f, -0.01071167f, 0.23291016f,  0.49731445f,  0.30331421f,  0.01318359f,
+        -0.02093506f, -0.00027466f, -0.01452637f, -0.01141357f, 0.23010254f,  0.49685669f,
+        0.30609131f,  0.01437378f,  -0.02117920f, -0.00030518f, -0.01428223f, -0.01211548f,
+        0.22732544f,  0.49636841f,  0.30886841f,  0.01559448f,  -0.02142334f, -0.00033569f,
+        -0.01403809f, -0.01278687f, 0.22451782f,  0.49581909f,  0.31164551f,  0.01684570f,
+        -0.02163696f, -0.00039673f, -0.01379395f, -0.01345825f, 0.22174072f,  0.49526978f,
+        0.31442261f,  0.01809692f,  -0.02188110f, -0.00042725f, -0.01358032f, -0.01409912f,
+        0.21896362f,  0.49465942f,  0.31719971f,  0.01937866f,  -0.02209473f, -0.00045776f,
+        -0.01333618f, -0.01473999f, 0.21618652f,  0.49404907f,  0.31994629f,  0.02069092f,
+        -0.02233887f, -0.00048828f, -0.01309204f, -0.01535034f, 0.21343994f,  0.49337769f,
+        0.32269287f,  0.02203369f,  -0.02255249f, -0.00054932f, -0.01284790f, -0.01596069f,
+        0.21066284f,  0.49267578f,  0.32543945f,  0.02337646f,  -0.02279663f, -0.00057983f,
+        -0.01263428f, -0.01654053f, 0.20791626f,  0.49194336f,  0.32818604f,  0.02471924f,
+        -0.02301025f, -0.00064087f, -0.01239014f, -0.01708984f, 0.20516968f,  0.49118042f,
+        0.33090210f,  0.02612305f,  -0.02322388f, -0.00067139f, -0.01214600f, -0.01763916f,
+        0.20242310f,  0.49035645f,  0.33361816f,  0.02752686f,  -0.02343750f, -0.00073242f,
+        -0.01193237f, -0.01818848f, 0.19970703f,  0.48953247f,  0.33633423f,  0.02896118f,
+        -0.02365112f, -0.00079346f, -0.01168823f, -0.01867676f, 0.19696045f,  0.48864746f,
+        0.33901978f,  0.03039551f,  -0.02386475f, -0.00082397f, -0.01147461f, -0.01919556f,
+        0.19427490f,  0.48776245f,  0.34170532f,  0.03186035f,  -0.02407837f, -0.00088501f,
+        -0.01123047f, -0.01968384f, 0.19155884f,  0.48681641f,  0.34439087f,  0.03335571f,
+        -0.02429199f, -0.00094604f, -0.01101685f, -0.02014160f, 0.18887329f,  0.48583984f,
+        0.34704590f,  0.03485107f,  -0.02447510f, -0.00100708f, -0.01080322f, -0.02059937f,
+        0.18615723f,  0.48483276f,  0.34970093f,  0.03637695f,  -0.02468872f, -0.00106812f,
+        -0.01058960f, -0.02102661f, 0.18350220f,  0.48379517f,  0.35235596f,  0.03793335f,
+        -0.02487183f, -0.00112915f, -0.01034546f, -0.02145386f, 0.18081665f,  0.48272705f,
+        0.35498047f,  0.03948975f,  -0.02505493f, -0.00119019f, -0.01013184f, -0.02188110f,
+        0.17816162f,  0.48162842f,  0.35760498f,  0.04107666f,  -0.02523804f, -0.00125122f,
+        -0.00991821f, -0.02227783f, 0.17550659f,  0.48049927f,  0.36019897f,  0.04269409f,
+        -0.02542114f, -0.00131226f, -0.00970459f, -0.02264404f, 0.17288208f,  0.47933960f,
+        0.36279297f,  0.04431152f,  -0.02560425f, -0.00140381f, -0.00952148f, -0.02301025f,
+        0.17025757f,  0.47814941f,  0.36538696f,  0.04595947f,  -0.02578735f, -0.00146484f,
+        -0.00930786f, -0.02337646f, 0.16763306f,  0.47689819f,  0.36795044f,  0.04763794f,
+        -0.02593994f, -0.00152588f, -0.00909424f, -0.02371216f, 0.16503906f,  0.47564697f,
+        0.37048340f,  0.04931641f,  -0.02609253f, -0.00161743f, -0.00888062f, -0.02401733f,
+        0.16244507f,  0.47436523f,  0.37304688f,  0.05102539f,  -0.02627563f, -0.00170898f,
+        -0.00869751f, -0.02435303f, 0.15988159f,  0.47302246f,  0.37554932f,  0.05276489f,
+        -0.02642822f, -0.00177002f, -0.00848389f, -0.02462769f, 0.15731812f,  0.47167969f,
+        0.37805176f,  0.05450439f,  -0.02658081f, -0.00186157f, -0.00830078f, -0.02493286f,
+        0.15475464f,  0.47027588f,  0.38055420f,  0.05627441f,  -0.02670288f, -0.00195312f,
+        -0.00808716f, -0.02520752f, 0.15222168f,  0.46887207f,  0.38302612f,  0.05804443f,
+        -0.02685547f, -0.00204468f, -0.00790405f, -0.02545166f, 0.14968872f,  0.46743774f,
+        0.38546753f,  0.05987549f,  -0.02697754f, -0.00213623f, -0.00772095f, -0.02569580f,
+        0.14718628f,  0.46594238f,  0.38790894f,  0.06170654f,  -0.02709961f, -0.00222778f,
+        -0.00753784f, -0.02593994f, 0.14468384f,  0.46444702f,  0.39031982f,  0.06353760f,
+        -0.02722168f, -0.00231934f, -0.00735474f, -0.02615356f, 0.14218140f,  0.46289062f,
+        0.39273071f,  0.06539917f,  -0.02734375f, -0.00241089f, -0.00717163f, -0.02636719f,
+        0.13970947f,  0.46133423f,  0.39511108f,  0.06729126f,  -0.02743530f, -0.00250244f,
+        -0.00698853f, -0.02655029f, 0.13726807f,  0.45974731f,  0.39749146f,  0.06918335f,
+        -0.02755737f, -0.00259399f, -0.00680542f, -0.02673340f, 0.13479614f,  0.45812988f,
+        0.39984131f,  0.07113647f,  -0.02764893f, -0.00271606f, -0.00662231f, -0.02691650f,
+        0.13238525f,  0.45648193f,  0.40216064f,  0.07305908f,  -0.02774048f, -0.00280762f,
+        -0.00643921f, -0.02706909f, 0.12997437f,  0.45480347f,  0.40447998f,  0.07504272f,
+        -0.02780151f, -0.00292969f, -0.00628662f, -0.02722168f, 0.12756348f,  0.45309448f,
+        0.40676880f,  0.07699585f,  -0.02789307f, -0.00305176f, -0.00610352f, -0.02734375f,
+        0.12518311f,  0.45135498f,  0.40902710f,  0.07901001f,  -0.02795410f, -0.00314331f,
+        -0.00595093f, -0.02746582f, 0.12280273f,  0.44958496f,  0.41128540f,  0.08102417f,
+        -0.02801514f, -0.00326538f, -0.00579834f, -0.02758789f, 0.12045288f,  0.44778442f,
+        0.41351318f,  0.08306885f,  -0.02804565f, -0.00338745f, -0.00561523f, -0.02770996f,
+        0.11813354f,  0.44598389f,  0.41571045f,  0.08511353f,  -0.02810669f, -0.00350952f,
+        -0.00546265f, -0.02780151f, 0.11581421f,  0.44412231f,  0.41787720f,  0.08718872f,
+        -0.02813721f, -0.00363159f, -0.00531006f, -0.02786255f, 0.11349487f,  0.44226074f,
+        0.42004395f,  0.08929443f,  -0.02816772f, -0.00375366f, -0.00515747f, -0.02795410f,
+        0.11120605f,  0.44036865f,  0.42218018f,  0.09140015f,  -0.02816772f, -0.00387573f,
+        -0.00500488f, -0.02801514f, 0.10894775f,  0.43844604f,  0.42431641f,  0.09353638f,
+        -0.02819824f, -0.00402832f, -0.00485229f, -0.02807617f, 0.10668945f,  0.43649292f,
+        0.42639160f,  0.09570312f,  -0.02819824f, -0.00415039f, -0.00469971f, -0.02810669f,
+        0.10446167f,  0.43453979f,  0.42846680f,  0.09786987f,  -0.02819824f, -0.00427246f,
+        -0.00457764f, -0.02813721f, 0.10223389f,  0.43252563f,  0.43051147f,  0.10003662f,
+        -0.02816772f, -0.00442505f, -0.00442505f, -0.02816772f, 0.10003662f,  0.43051147f,
+        0.43252563f,  0.10223389f,  -0.02813721f, -0.00457764f, -0.00427246f, -0.02819824f,
+        0.09786987f,  0.42846680f,  0.43453979f,  0.10446167f,  -0.02810669f, -0.00469971f,
+        -0.00415039f, -0.02819824f, 0.09570312f,  0.42639160f,  0.43649292f,  0.10668945f,
+        -0.02807617f, -0.00485229f, -0.00402832f, -0.02819824f, 0.09353638f,  0.42431641f,
+        0.43844604f,  0.10894775f,  -0.02801514f, -0.00500488f, -0.00387573f, -0.02816772f,
+        0.09140015f,  0.42218018f,  0.44036865f,  0.11120605f,  -0.02795410f, -0.00515747f,
+        -0.00375366f, -0.02816772f, 0.08929443f,  0.42004395f,  0.44226074f,  0.11349487f,
+        -0.02786255f, -0.00531006f, -0.00363159f, -0.02813721f, 0.08718872f,  0.41787720f,
+        0.44412231f,  0.11581421f,  -0.02780151f, -0.00546265f, -0.00350952f, -0.02810669f,
+        0.08511353f,  0.41571045f,  0.44598389f,  0.11813354f,  -0.02770996f, -0.00561523f,
+        -0.00338745f, -0.02804565f, 0.08306885f,  0.41351318f,  0.44778442f,  0.12045288f,
+        -0.02758789f, -0.00579834f, -0.00326538f, -0.02801514f, 0.08102417f,  0.41128540f,
+        0.44958496f,  0.12280273f,  -0.02746582f, -0.00595093f, -0.00314331f, -0.02795410f,
+        0.07901001f,  0.40902710f,  0.45135498f,  0.12518311f,  -0.02734375f, -0.00610352f,
+        -0.00305176f, -0.02789307f, 0.07699585f,  0.40676880f,  0.45309448f,  0.12756348f,
+        -0.02722168f, -0.00628662f, -0.00292969f, -0.02780151f, 0.07504272f,  0.40447998f,
+        0.45480347f,  0.12997437f,  -0.02706909f, -0.00643921f, -0.00280762f, -0.02774048f,
+        0.07305908f,  0.40216064f,  0.45648193f,  0.13238525f,  -0.02691650f, -0.00662231f,
+        -0.00271606f, -0.02764893f, 0.07113647f,  0.39984131f,  0.45812988f,  0.13479614f,
+        -0.02673340f, -0.00680542f, -0.00259399f, -0.02755737f, 0.06918335f,  0.39749146f,
+        0.45974731f,  0.13726807f,  -0.02655029f, -0.00698853f, -0.00250244f, -0.02743530f,
+        0.06729126f,  0.39511108f,  0.46133423f,  0.13970947f,  -0.02636719f, -0.00717163f,
+        -0.00241089f, -0.02734375f, 0.06539917f,  0.39273071f,  0.46289062f,  0.14218140f,
+        -0.02615356f, -0.00735474f, -0.00231934f, -0.02722168f, 0.06353760f,  0.39031982f,
+        0.46444702f,  0.14468384f,  -0.02593994f, -0.00753784f, -0.00222778f, -0.02709961f,
+        0.06170654f,  0.38790894f,  0.46594238f,  0.14718628f,  -0.02569580f, -0.00772095f,
+        -0.00213623f, -0.02697754f, 0.05987549f,  0.38546753f,  0.46743774f,  0.14968872f,
+        -0.02545166f, -0.00790405f, -0.00204468f, -0.02685547f, 0.05804443f,  0.38302612f,
+        0.46887207f,  0.15222168f,  -0.02520752f, -0.00808716f, -0.00195312f, -0.02670288f,
+        0.05627441f,  0.38055420f,  0.47027588f,  0.15475464f,  -0.02493286f, -0.00830078f,
+        -0.00186157f, -0.02658081f, 0.05450439f,  0.37805176f,  0.47167969f,  0.15731812f,
+        -0.02462769f, -0.00848389f, -0.00177002f, -0.02642822f, 0.05276489f,  0.37554932f,
+        0.47302246f,  0.15988159f,  -0.02435303f, -0.00869751f, -0.00170898f, -0.02627563f,
+        0.05102539f,  0.37304688f,  0.47436523f,  0.16244507f,  -0.02401733f, -0.00888062f,
+        -0.00161743f, -0.02609253f, 0.04931641f,  0.37048340f,  0.47564697f,  0.16503906f,
+        -0.02371216f, -0.00909424f, -0.00152588f, -0.02593994f, 0.04763794f,  0.36795044f,
+        0.47689819f,  0.16763306f,  -0.02337646f, -0.00930786f, -0.00146484f, -0.02578735f,
+        0.04595947f,  0.36538696f,  0.47814941f,  0.17025757f,  -0.02301025f, -0.00952148f,
+        -0.00140381f, -0.02560425f, 0.04431152f,  0.36279297f,  0.47933960f,  0.17288208f,
+        -0.02264404f, -0.00970459f, -0.00131226f, -0.02542114f, 0.04269409f,  0.36019897f,
+        0.48049927f,  0.17550659f,  -0.02227783f, -0.00991821f, -0.00125122f, -0.02523804f,
+        0.04107666f,  0.35760498f,  0.48162842f,  0.17816162f,  -0.02188110f, -0.01013184f,
+        -0.00119019f, -0.02505493f, 0.03948975f,  0.35498047f,  0.48272705f,  0.18081665f,
+        -0.02145386f, -0.01034546f, -0.00112915f, -0.02487183f, 0.03793335f,  0.35235596f,
+        0.48379517f,  0.18350220f,  -0.02102661f, -0.01058960f, -0.00106812f, -0.02468872f,
+        0.03637695f,  0.34970093f,  0.48483276f,  0.18615723f,  -0.02059937f, -0.01080322f,
+        -0.00100708f, -0.02447510f, 0.03485107f,  0.34704590f,  0.48583984f,  0.18887329f,
+        -0.02014160f, -0.01101685f, -0.00094604f, -0.02429199f, 0.03335571f,  0.34439087f,
+        0.48681641f,  0.19155884f,  -0.01968384f, -0.01123047f, -0.00088501f, -0.02407837f,
+        0.03186035f,  0.34170532f,  0.48776245f,  0.19427490f,  -0.01919556f, -0.01147461f,
+        -0.00082397f, -0.02386475f, 0.03039551f,  0.33901978f,  0.48864746f,  0.19696045f,
+        -0.01867676f, -0.01168823f, -0.00079346f, -0.02365112f, 0.02896118f,  0.33633423f,
+        0.48953247f,  0.19970703f,  -0.01818848f, -0.01193237f, -0.00073242f, -0.02343750f,
+        0.02752686f,  0.33361816f,  0.49035645f,  0.20242310f,  -0.01763916f, -0.01214600f,
+        -0.00067139f, -0.02322388f, 0.02612305f,  0.33090210f,  0.49118042f,  0.20516968f,
+        -0.01708984f, -0.01239014f, -0.00064087f, -0.02301025f, 0.02471924f,  0.32818604f,
+        0.49194336f,  0.20791626f,  -0.01654053f, -0.01263428f, -0.00057983f, -0.02279663f,
+        0.02337646f,  0.32543945f,  0.49267578f,  0.21066284f,  -0.01596069f, -0.01284790f,
+        -0.00054932f, -0.02255249f, 0.02203369f,  0.32269287f,  0.49337769f,  0.21343994f,
+        -0.01535034f, -0.01309204f, -0.00048828f, -0.02233887f, 0.02069092f,  0.31994629f,
+        0.49404907f,  0.21618652f,  -0.01473999f, -0.01333618f, -0.00045776f, -0.02209473f,
+        0.01937866f,  0.31719971f,  0.49465942f,  0.21896362f,  -0.01409912f, -0.01358032f,
+        -0.00042725f, -0.02188110f, 0.01809692f,  0.31442261f,  0.49526978f,  0.22174072f,
+        -0.01345825f, -0.01379395f, -0.00039673f, -0.02163696f, 0.01684570f,  0.31164551f,
+        0.49581909f,  0.22451782f,  -0.01278687f, -0.01403809f, -0.00033569f, -0.02142334f,
+        0.01559448f,  0.30886841f,  0.49636841f,  0.22732544f,  -0.01211548f, -0.01428223f,
+        -0.00030518f, -0.02117920f, 0.01437378f,  0.30609131f,  0.49685669f,  0.23010254f,
+        -0.01141357f, -0.01452637f, -0.00027466f, -0.02093506f, 0.01318359f,  0.30331421f,
+        0.49731445f,  0.23291016f,  -0.01071167f, -0.01477051f, -0.00024414f, -0.02069092f,
+        0.01199341f,  0.30050659f,  0.49774170f,  0.23571777f,  -0.00997925f, -0.01501465f,
+        -0.00021362f, -0.02044678f, 0.01083374f,  0.29772949f,  0.49810791f,  0.23852539f,
+        -0.00921631f, -0.01525879f, -0.00018311f, -0.02020264f, 0.00967407f,  0.29492188f,
+        0.49847412f,  0.24133301f,  -0.00845337f, -0.01550293f, -0.00015259f, -0.01995850f,
+        0.00854492f,  0.29211426f,  0.49877930f,  0.24414062f,  -0.00765991f, -0.01574707f,
+        -0.00015259f, -0.01971436f, 0.00744629f,  0.28930664f,  0.49908447f,  0.24697876f,
+        -0.00686646f, -0.01602173f, -0.00012207f, -0.01947021f, 0.00634766f,  0.28646851f,
+        0.49932861f,  0.24978638f,  -0.00604248f, -0.01626587f, -0.00009155f, -0.01922607f,
+        0.00527954f,  0.28366089f,  0.49954224f,  0.25259399f,  -0.00518799f, -0.01651001f,
+        -0.00006104f, -0.01898193f, 0.00424194f,  0.28085327f,  0.49972534f,  0.25543213f,
+        -0.00433350f, -0.01675415f, -0.00006104f, -0.01873779f, 0.00320435f,  0.27801514f,
+        0.49984741f,  0.25823975f,  -0.00344849f, -0.01699829f, -0.00003052f, -0.01849365f,
+        0.00219727f,  0.27520752f,  0.49996948f,  0.26107788f,  -0.00256348f, -0.01724243f,
+        -0.00003052f, -0.01824951f, 0.00122070f,  0.27236938f,  0.50003052f,  0.26388550f,
+        -0.00164795f, -0.01748657f, 0.00000000f,  -0.01800537f, 0.00024414f,  0.26956177f,
+        0.50006104f,  0.26672363f,  -0.00070190f, -0.01776123f,
+    };
+
+    static constexpr std::array<f32, 1024> lut1 = {
+        0.01275635f,  -0.07745361f, 0.18670654f,  0.75119019f,  0.19219971f,  -0.07821655f,
+        0.01272583f,  0.00000000f,  0.01281738f,  -0.07666016f, 0.18124390f,  0.75106812f,
+        0.19772339f,  -0.07897949f, 0.01266479f,  0.00003052f,  0.01284790f,  -0.07583618f,
+        0.17581177f,  0.75088501f,  0.20330811f,  -0.07971191f, 0.01257324f,  0.00006104f,
+        0.01287842f,  -0.07501221f, 0.17044067f,  0.75057983f,  0.20892334f,  -0.08041382f,
+        0.01248169f,  0.00009155f,  0.01290894f,  -0.07415771f, 0.16510010f,  0.75018311f,
+        0.21453857f,  -0.08111572f, 0.01239014f,  0.00012207f,  0.01290894f,  -0.07330322f,
+        0.15979004f,  0.74966431f,  0.22021484f,  -0.08178711f, 0.01229858f,  0.00015259f,
+        0.01290894f,  -0.07241821f, 0.15454102f,  0.74908447f,  0.22592163f,  -0.08242798f,
+        0.01217651f,  0.00018311f,  0.01290894f,  -0.07150269f, 0.14932251f,  0.74838257f,
+        0.23165894f,  -0.08303833f, 0.01205444f,  0.00021362f,  0.01290894f,  -0.07058716f,
+        0.14416504f,  0.74755859f,  0.23742676f,  -0.08364868f, 0.01193237f,  0.00024414f,
+        0.01287842f,  -0.06967163f, 0.13903809f,  0.74667358f,  0.24322510f,  -0.08419800f,
+        0.01177979f,  0.00027466f,  0.01284790f,  -0.06872559f, 0.13397217f,  0.74566650f,
+        0.24905396f,  -0.08474731f, 0.01162720f,  0.00033569f,  0.01281738f,  -0.06777954f,
+        0.12893677f,  0.74456787f,  0.25491333f,  -0.08526611f, 0.01147461f,  0.00036621f,
+        0.01278687f,  -0.06683350f, 0.12396240f,  0.74337769f,  0.26077271f,  -0.08575439f,
+        0.01129150f,  0.00042725f,  0.01275635f,  -0.06585693f, 0.11901855f,  0.74206543f,
+        0.26669312f,  -0.08621216f, 0.01110840f,  0.00045776f,  0.01269531f,  -0.06488037f,
+        0.11413574f,  0.74069214f,  0.27261353f,  -0.08663940f, 0.01092529f,  0.00051880f,
+        0.01263428f,  -0.06387329f, 0.10931396f,  0.73919678f,  0.27853394f,  -0.08700562f,
+        0.01071167f,  0.00057983f,  0.01257324f,  -0.06286621f, 0.10452271f,  0.73760986f,
+        0.28451538f,  -0.08737183f, 0.01049805f,  0.00064087f,  0.01251221f,  -0.06185913f,
+        0.09979248f,  0.73593140f,  0.29049683f,  -0.08770752f, 0.01025391f,  0.00067139f,
+        0.01242065f,  -0.06082153f, 0.09512329f,  0.73413086f,  0.29647827f,  -0.08801270f,
+        0.01000977f,  0.00073242f,  0.01232910f,  -0.05981445f, 0.09051514f,  0.73226929f,
+        0.30249023f,  -0.08828735f, 0.00973511f,  0.00079346f,  0.01226807f,  -0.05877686f,
+        0.08593750f,  0.73028564f,  0.30853271f,  -0.08850098f, 0.00949097f,  0.00088501f,
+        0.01214600f,  -0.05773926f, 0.08142090f,  0.72824097f,  0.31457520f,  -0.08871460f,
+        0.00918579f,  0.00094604f,  0.01205444f,  -0.05670166f, 0.07696533f,  0.72607422f,
+        0.32061768f,  -0.08886719f, 0.00891113f,  0.00100708f,  0.01196289f,  -0.05563354f,
+        0.07257080f,  0.72381592f,  0.32669067f,  -0.08898926f, 0.00860596f,  0.00106812f,
+        0.01187134f,  -0.05459595f, 0.06820679f,  0.72146606f,  0.33276367f,  -0.08908081f,
+        0.00827026f,  0.00115967f,  0.01174927f,  -0.05352783f, 0.06393433f,  0.71902466f,
+        0.33883667f,  -0.08911133f, 0.00796509f,  0.00122070f,  0.01162720f,  -0.05245972f,
+        0.05969238f,  0.71649170f,  0.34494019f,  -0.08914185f, 0.00759888f,  0.00131226f,
+        0.01150513f,  -0.05139160f, 0.05551147f,  0.71389771f,  0.35101318f,  -0.08911133f,
+        0.00726318f,  0.00137329f,  0.01138306f,  -0.05032349f, 0.05139160f,  0.71118164f,
+        0.35711670f,  -0.08901978f, 0.00686646f,  0.00146484f,  0.01126099f,  -0.04928589f,
+        0.04733276f,  0.70837402f,  0.36322021f,  -0.08892822f, 0.00650024f,  0.00155640f,
+        0.01113892f,  -0.04821777f, 0.04333496f,  0.70550537f,  0.36932373f,  -0.08877563f,
+        0.00610352f,  0.00164795f,  0.01101685f,  -0.04714966f, 0.03939819f,  0.70251465f,
+        0.37542725f,  -0.08856201f, 0.00567627f,  0.00173950f,  0.01086426f,  -0.04608154f,
+        0.03549194f,  0.69946289f,  0.38153076f,  -0.08834839f, 0.00527954f,  0.00183105f,
+        0.01074219f,  -0.04501343f, 0.03167725f,  0.69631958f,  0.38763428f,  -0.08804321f,
+        0.00482178f,  0.00192261f,  0.01058960f,  -0.04394531f, 0.02792358f,  0.69308472f,
+        0.39370728f,  -0.08773804f, 0.00436401f,  0.00201416f,  0.01043701f,  -0.04287720f,
+        0.02420044f,  0.68975830f,  0.39981079f,  -0.08737183f, 0.00390625f,  0.00210571f,
+        0.01031494f,  -0.04180908f, 0.02056885f,  0.68637085f,  0.40588379f,  -0.08694458f,
+        0.00344849f,  0.00222778f,  0.01016235f,  -0.04074097f, 0.01699829f,  0.68289185f,
+        0.41195679f,  -0.08648682f, 0.00296021f,  0.00231934f,  0.01000977f,  -0.03970337f,
+        0.01345825f,  0.67932129f,  0.41802979f,  -0.08596802f, 0.00244141f,  0.00244141f,
+        0.00985718f,  -0.03863525f, 0.01000977f,  0.67568970f,  0.42407227f,  -0.08541870f,
+        0.00192261f,  0.00253296f,  0.00970459f,  -0.03759766f, 0.00662231f,  0.67196655f,
+        0.43011475f,  -0.08480835f, 0.00140381f,  0.00265503f,  0.00955200f,  -0.03652954f,
+        0.00326538f,  0.66815186f,  0.43612671f,  -0.08416748f, 0.00085449f,  0.00277710f,
+        0.00936890f,  -0.03549194f, 0.00000000f,  0.66427612f,  0.44213867f,  -0.08346558f,
+        0.00027466f,  0.00289917f,  0.00921631f,  -0.03445435f, -0.00320435f, 0.66030884f,
+        0.44812012f,  -0.08270264f, -0.00027466f, 0.00299072f,  0.00906372f,  -0.03344727f,
+        -0.00634766f, 0.65631104f,  0.45407104f,  -0.08190918f, -0.00088501f, 0.00311279f,
+        0.00891113f,  -0.03240967f, -0.00946045f, 0.65219116f,  0.46002197f,  -0.08105469f,
+        -0.00146484f, 0.00323486f,  0.00872803f,  -0.03140259f, -0.01248169f, 0.64801025f,
+        0.46594238f,  -0.08013916f, -0.00210571f, 0.00338745f,  0.00857544f,  -0.03039551f,
+        -0.01544189f, 0.64376831f,  0.47183228f,  -0.07919312f, -0.00271606f, 0.00350952f,
+        0.00842285f,  -0.02938843f, -0.01834106f, 0.63946533f,  0.47772217f,  -0.07818604f,
+        -0.00335693f, 0.00363159f,  0.00823975f,  -0.02838135f, -0.02117920f, 0.63507080f,
+        0.48358154f,  -0.07711792f, -0.00402832f, 0.00375366f,  0.00808716f,  -0.02740479f,
+        -0.02395630f, 0.63061523f,  0.48937988f,  -0.07598877f, -0.00469971f, 0.00390625f,
+        0.00793457f,  -0.02642822f, -0.02667236f, 0.62609863f,  0.49517822f,  -0.07482910f,
+        -0.00537109f, 0.00402832f,  0.00775146f,  -0.02545166f, -0.02932739f, 0.62152100f,
+        0.50094604f,  -0.07357788f, -0.00607300f, 0.00418091f,  0.00759888f,  -0.02450562f,
+        -0.03192139f, 0.61685181f,  0.50665283f,  -0.07229614f, -0.00677490f, 0.00430298f,
+        0.00741577f,  -0.02352905f, -0.03445435f, 0.61215210f,  0.51235962f,  -0.07098389f,
+        -0.00750732f, 0.00445557f,  0.00726318f,  -0.02258301f, -0.03689575f, 0.60736084f,
+        0.51800537f,  -0.06958008f, -0.00823975f, 0.00460815f,  0.00711060f,  -0.02166748f,
+        -0.03930664f, 0.60253906f,  0.52362061f,  -0.06811523f, -0.00897217f, 0.00476074f,
+        0.00692749f,  -0.02075195f, -0.04165649f, 0.59762573f,  0.52920532f,  -0.06661987f,
+        -0.00973511f, 0.00488281f,  0.00677490f,  -0.01983643f, -0.04394531f, 0.59268188f,
+        0.53475952f,  -0.06506348f, -0.01052856f, 0.00503540f,  0.00662231f,  -0.01892090f,
+        -0.04617310f, 0.58767700f,  0.54025269f,  -0.06344604f, -0.01129150f, 0.00518799f,
+        0.00643921f,  -0.01803589f, -0.04830933f, 0.58261108f,  0.54571533f,  -0.06173706f,
+        -0.01208496f, 0.00534058f,  0.00628662f,  -0.01715088f, -0.05041504f, 0.57748413f,
+        0.55111694f,  -0.05999756f, -0.01290894f, 0.00549316f,  0.00613403f,  -0.01626587f,
+        -0.05245972f, 0.57232666f,  0.55648804f,  -0.05819702f, -0.01373291f, 0.00564575f,
+        0.00598145f,  -0.01541138f, -0.05444336f, 0.56707764f,  0.56182861f,  -0.05636597f,
+        -0.01455688f, 0.00582886f,  0.00582886f,  -0.01455688f, -0.05636597f, 0.56182861f,
+        0.56707764f,  -0.05444336f, -0.01541138f, 0.00598145f,  0.00564575f,  -0.01373291f,
+        -0.05819702f, 0.55648804f,  0.57232666f,  -0.05245972f, -0.01626587f, 0.00613403f,
+        0.00549316f,  -0.01290894f, -0.05999756f, 0.55111694f,  0.57748413f,  -0.05041504f,
+        -0.01715088f, 0.00628662f,  0.00534058f,  -0.01208496f, -0.06173706f, 0.54571533f,
+        0.58261108f,  -0.04830933f, -0.01803589f, 0.00643921f,  0.00518799f,  -0.01129150f,
+        -0.06344604f, 0.54025269f,  0.58767700f,  -0.04617310f, -0.01892090f, 0.00662231f,
+        0.00503540f,  -0.01052856f, -0.06506348f, 0.53475952f,  0.59268188f,  -0.04394531f,
+        -0.01983643f, 0.00677490f,  0.00488281f,  -0.00973511f, -0.06661987f, 0.52920532f,
+        0.59762573f,  -0.04165649f, -0.02075195f, 0.00692749f,  0.00476074f,  -0.00897217f,
+        -0.06811523f, 0.52362061f,  0.60253906f,  -0.03930664f, -0.02166748f, 0.00711060f,
+        0.00460815f,  -0.00823975f, -0.06958008f, 0.51800537f,  0.60736084f,  -0.03689575f,
+        -0.02258301f, 0.00726318f,  0.00445557f,  -0.00750732f, -0.07098389f, 0.51235962f,
+        0.61215210f,  -0.03445435f, -0.02352905f, 0.00741577f,  0.00430298f,  -0.00677490f,
+        -0.07229614f, 0.50665283f,  0.61685181f,  -0.03192139f, -0.02450562f, 0.00759888f,
+        0.00418091f,  -0.00607300f, -0.07357788f, 0.50094604f,  0.62152100f,  -0.02932739f,
+        -0.02545166f, 0.00775146f,  0.00402832f,  -0.00537109f, -0.07482910f, 0.49517822f,
+        0.62609863f,  -0.02667236f, -0.02642822f, 0.00793457f,  0.00390625f,  -0.00469971f,
+        -0.07598877f, 0.48937988f,  0.63061523f,  -0.02395630f, -0.02740479f, 0.00808716f,
+        0.00375366f,  -0.00402832f, -0.07711792f, 0.48358154f,  0.63507080f,  -0.02117920f,
+        -0.02838135f, 0.00823975f,  0.00363159f,  -0.00335693f, -0.07818604f, 0.47772217f,
+        0.63946533f,  -0.01834106f, -0.02938843f, 0.00842285f,  0.00350952f,  -0.00271606f,
+        -0.07919312f, 0.47183228f,  0.64376831f,  -0.01544189f, -0.03039551f, 0.00857544f,
+        0.00338745f,  -0.00210571f, -0.08013916f, 0.46594238f,  0.64801025f,  -0.01248169f,
+        -0.03140259f, 0.00872803f,  0.00323486f,  -0.00146484f, -0.08105469f, 0.46002197f,
+        0.65219116f,  -0.00946045f, -0.03240967f, 0.00891113f,  0.00311279f,  -0.00088501f,
+        -0.08190918f, 0.45407104f,  0.65631104f,  -0.00634766f, -0.03344727f, 0.00906372f,
+        0.00299072f,  -0.00027466f, -0.08270264f, 0.44812012f,  0.66030884f,  -0.00320435f,
+        -0.03445435f, 0.00921631f,  0.00289917f,  0.00027466f,  -0.08346558f, 0.44213867f,
+        0.66427612f,  0.00000000f,  -0.03549194f, 0.00936890f,  0.00277710f,  0.00085449f,
+        -0.08416748f, 0.43612671f,  0.66815186f,  0.00326538f,  -0.03652954f, 0.00955200f,
+        0.00265503f,  0.00140381f,  -0.08480835f, 0.43011475f,  0.67196655f,  0.00662231f,
+        -0.03759766f, 0.00970459f,  0.00253296f,  0.00192261f,  -0.08541870f, 0.42407227f,
+        0.67568970f,  0.01000977f,  -0.03863525f, 0.00985718f,  0.00244141f,  0.00244141f,
+        -0.08596802f, 0.41802979f,  0.67932129f,  0.01345825f,  -0.03970337f, 0.01000977f,
+        0.00231934f,  0.00296021f,  -0.08648682f, 0.41195679f,  0.68289185f,  0.01699829f,
+        -0.04074097f, 0.01016235f,  0.00222778f,  0.00344849f,  -0.08694458f, 0.40588379f,
+        0.68637085f,  0.02056885f,  -0.04180908f, 0.01031494f,  0.00210571f,  0.00390625f,
+        -0.08737183f, 0.39981079f,  0.68975830f,  0.02420044f,  -0.04287720f, 0.01043701f,
+        0.00201416f,  0.00436401f,  -0.08773804f, 0.39370728f,  0.69308472f,  0.02792358f,
+        -0.04394531f, 0.01058960f,  0.00192261f,  0.00482178f,  -0.08804321f, 0.38763428f,
+        0.69631958f,  0.03167725f,  -0.04501343f, 0.01074219f,  0.00183105f,  0.00527954f,
+        -0.08834839f, 0.38153076f,  0.69946289f,  0.03549194f,  -0.04608154f, 0.01086426f,
+        0.00173950f,  0.00567627f,  -0.08856201f, 0.37542725f,  0.70251465f,  0.03939819f,
+        -0.04714966f, 0.01101685f,  0.00164795f,  0.00610352f,  -0.08877563f, 0.36932373f,
+        0.70550537f,  0.04333496f,  -0.04821777f, 0.01113892f,  0.00155640f,  0.00650024f,
+        -0.08892822f, 0.36322021f,  0.70837402f,  0.04733276f,  -0.04928589f, 0.01126099f,
+        0.00146484f,  0.00686646f,  -0.08901978f, 0.35711670f,  0.71118164f,  0.05139160f,
+        -0.05032349f, 0.01138306f,  0.00137329f,  0.00726318f,  -0.08911133f, 0.35101318f,
+        0.71389771f,  0.05551147f,  -0.05139160f, 0.01150513f,  0.00131226f,  0.00759888f,
+        -0.08914185f, 0.34494019f,  0.71649170f,  0.05969238f,  -0.05245972f, 0.01162720f,
+        0.00122070f,  0.00796509f,  -0.08911133f, 0.33883667f,  0.71902466f,  0.06393433f,
+        -0.05352783f, 0.01174927f,  0.00115967f,  0.00827026f,  -0.08908081f, 0.33276367f,
+        0.72146606f,  0.06820679f,  -0.05459595f, 0.01187134f,  0.00106812f,  0.00860596f,
+        -0.08898926f, 0.32669067f,  0.72381592f,  0.07257080f,  -0.05563354f, 0.01196289f,
+        0.00100708f,  0.00891113f,  -0.08886719f, 0.32061768f,  0.72607422f,  0.07696533f,
+        -0.05670166f, 0.01205444f,  0.00094604f,  0.00918579f,  -0.08871460f, 0.31457520f,
+        0.72824097f,  0.08142090f,  -0.05773926f, 0.01214600f,  0.00088501f,  0.00949097f,
+        -0.08850098f, 0.30853271f,  0.73028564f,  0.08593750f,  -0.05877686f, 0.01226807f,
+        0.00079346f,  0.00973511f,  -0.08828735f, 0.30249023f,  0.73226929f,  0.09051514f,
+        -0.05981445f, 0.01232910f,  0.00073242f,  0.01000977f,  -0.08801270f, 0.29647827f,
+        0.73413086f,  0.09512329f,  -0.06082153f, 0.01242065f,  0.00067139f,  0.01025391f,
+        -0.08770752f, 0.29049683f,  0.73593140f,  0.09979248f,  -0.06185913f, 0.01251221f,
+        0.00064087f,  0.01049805f,  -0.08737183f, 0.28451538f,  0.73760986f,  0.10452271f,
+        -0.06286621f, 0.01257324f,  0.00057983f,  0.01071167f,  -0.08700562f, 0.27853394f,
+        0.73919678f,  0.10931396f,  -0.06387329f, 0.01263428f,  0.00051880f,  0.01092529f,
+        -0.08663940f, 0.27261353f,  0.74069214f,  0.11413574f,  -0.06488037f, 0.01269531f,
+        0.00045776f,  0.01110840f,  -0.08621216f, 0.26669312f,  0.74206543f,  0.11901855f,
+        -0.06585693f, 0.01275635f,  0.00042725f,  0.01129150f,  -0.08575439f, 0.26077271f,
+        0.74337769f,  0.12396240f,  -0.06683350f, 0.01278687f,  0.00036621f,  0.01147461f,
+        -0.08526611f, 0.25491333f,  0.74456787f,  0.12893677f,  -0.06777954f, 0.01281738f,
+        0.00033569f,  0.01162720f,  -0.08474731f, 0.24905396f,  0.74566650f,  0.13397217f,
+        -0.06872559f, 0.01284790f,  0.00027466f,  0.01177979f,  -0.08419800f, 0.24322510f,
+        0.74667358f,  0.13903809f,  -0.06967163f, 0.01287842f,  0.00024414f,  0.01193237f,
+        -0.08364868f, 0.23742676f,  0.74755859f,  0.14416504f,  -0.07058716f, 0.01290894f,
+        0.00021362f,  0.01205444f,  -0.08303833f, 0.23165894f,  0.74838257f,  0.14932251f,
+        -0.07150269f, 0.01290894f,  0.00018311f,  0.01217651f,  -0.08242798f, 0.22592163f,
+        0.74908447f,  0.15454102f,  -0.07241821f, 0.01290894f,  0.00015259f,  0.01229858f,
+        -0.08178711f, 0.22021484f,  0.74966431f,  0.15979004f,  -0.07330322f, 0.01290894f,
+        0.00012207f,  0.01239014f,  -0.08111572f, 0.21453857f,  0.75018311f,  0.16510010f,
+        -0.07415771f, 0.01290894f,  0.00009155f,  0.01248169f,  -0.08041382f, 0.20892334f,
+        0.75057983f,  0.17044067f,  -0.07501221f, 0.01287842f,  0.00006104f,  0.01257324f,
+        -0.07971191f, 0.20330811f,  0.75088501f,  0.17581177f,  -0.07583618f, 0.01284790f,
+        0.00003052f,  0.01266479f,  -0.07897949f, 0.19772339f,  0.75106812f,  0.18124390f,
+        -0.07666016f, 0.01281738f,  0.00000000f,  0.01272583f,  -0.07821655f, 0.19219971f,
+        0.75119019f,  0.18670654f,  -0.07745361f, 0.01275635f,
+    };
+
+    static constexpr std::array<f32, 1024> lut2 = {
+        -0.00036621f, 0.00143433f,  -0.00408936f, 0.99996948f,  0.00247192f,  -0.00048828f,
+        0.00006104f,  0.00000000f,  -0.00079346f, 0.00329590f,  -0.01052856f, 0.99975586f,
+        0.00918579f,  -0.00241089f, 0.00051880f,  -0.00003052f, -0.00122070f, 0.00512695f,
+        -0.01684570f, 0.99929810f,  0.01605225f,  -0.00439453f, 0.00097656f,  -0.00006104f,
+        -0.00161743f, 0.00689697f,  -0.02297974f, 0.99862671f,  0.02304077f,  -0.00640869f,
+        0.00143433f,  -0.00009155f, -0.00201416f, 0.00866699f,  -0.02899170f, 0.99774170f,
+        0.03018188f,  -0.00845337f, 0.00192261f,  -0.00015259f, -0.00238037f, 0.01037598f,
+        -0.03488159f, 0.99664307f,  0.03741455f,  -0.01055908f, 0.00241089f,  -0.00018311f,
+        -0.00274658f, 0.01202393f,  -0.04061890f, 0.99533081f,  0.04483032f,  -0.01266479f,
+        0.00292969f,  -0.00024414f, -0.00308228f, 0.01364136f,  -0.04620361f, 0.99377441f,
+        0.05233765f,  -0.01483154f, 0.00344849f,  -0.00027466f, -0.00341797f, 0.01522827f,
+        -0.05163574f, 0.99200439f,  0.05999756f,  -0.01699829f, 0.00396729f,  -0.00033569f,
+        -0.00375366f, 0.01678467f,  -0.05691528f, 0.99002075f,  0.06777954f,  -0.01922607f,
+        0.00451660f,  -0.00039673f, -0.00405884f, 0.01828003f,  -0.06207275f, 0.98782349f,
+        0.07568359f,  -0.02145386f, 0.00506592f,  -0.00042725f, -0.00436401f, 0.01971436f,
+        -0.06707764f, 0.98541260f,  0.08370972f,  -0.02374268f, 0.00564575f,  -0.00048828f,
+        -0.00463867f, 0.02114868f,  -0.07192993f, 0.98278809f,  0.09185791f,  -0.02603149f,
+        0.00622559f,  -0.00054932f, -0.00494385f, 0.02252197f,  -0.07666016f, 0.97991943f,
+        0.10012817f,  -0.02835083f, 0.00680542f,  -0.00061035f, -0.00518799f, 0.02383423f,
+        -0.08123779f, 0.97686768f,  0.10848999f,  -0.03073120f, 0.00738525f,  -0.00070190f,
+        -0.00543213f, 0.02511597f,  -0.08566284f, 0.97360229f,  0.11700439f,  -0.03308105f,
+        0.00799561f,  -0.00076294f, -0.00567627f, 0.02636719f,  -0.08993530f, 0.97012329f,
+        0.12561035f,  -0.03549194f, 0.00860596f,  -0.00082397f, -0.00592041f, 0.02755737f,
+        -0.09405518f, 0.96643066f,  0.13436890f,  -0.03790283f, 0.00924683f,  -0.00091553f,
+        -0.00613403f, 0.02868652f,  -0.09805298f, 0.96252441f,  0.14318848f,  -0.04034424f,
+        0.00985718f,  -0.00097656f, -0.00631714f, 0.02981567f,  -0.10189819f, 0.95843506f,
+        0.15213013f,  -0.04281616f, 0.01049805f,  -0.00106812f, -0.00653076f, 0.03085327f,
+        -0.10559082f, 0.95413208f,  0.16119385f,  -0.04528809f, 0.01113892f,  -0.00112915f,
+        -0.00671387f, 0.03189087f,  -0.10916138f, 0.94961548f,  0.17034912f,  -0.04779053f,
+        0.01181030f,  -0.00122070f, -0.00686646f, 0.03286743f,  -0.11254883f, 0.94491577f,
+        0.17959595f,  -0.05029297f, 0.01248169f,  -0.00131226f, -0.00701904f, 0.03378296f,
+        -0.11584473f, 0.94000244f,  0.18893433f,  -0.05279541f, 0.01315308f,  -0.00140381f,
+        -0.00717163f, 0.03466797f,  -0.11895752f, 0.93490601f,  0.19839478f,  -0.05532837f,
+        0.01382446f,  -0.00149536f, -0.00732422f, 0.03552246f,  -0.12194824f, 0.92962646f,
+        0.20791626f,  -0.05786133f, 0.01449585f,  -0.00158691f, -0.00744629f, 0.03631592f,
+        -0.12478638f, 0.92413330f,  0.21752930f,  -0.06042480f, 0.01519775f,  -0.00167847f,
+        -0.00753784f, 0.03707886f,  -0.12750244f, 0.91848755f,  0.22723389f,  -0.06298828f,
+        0.01586914f,  -0.00177002f, -0.00765991f, 0.03781128f,  -0.13006592f, 0.91262817f,
+        0.23703003f,  -0.06555176f, 0.01657104f,  -0.00189209f, -0.00775146f, 0.03848267f,
+        -0.13250732f, 0.90658569f,  0.24691772f,  -0.06808472f, 0.01727295f,  -0.00198364f,
+        -0.00784302f, 0.03909302f,  -0.13479614f, 0.90036011f,  0.25683594f,  -0.07064819f,
+        0.01797485f,  -0.00210571f, -0.00790405f, 0.03970337f,  -0.13696289f, 0.89395142f,
+        0.26687622f,  -0.07321167f, 0.01870728f,  -0.00219727f, -0.00796509f, 0.04025269f,
+        -0.13900757f, 0.88739014f,  0.27694702f,  -0.07577515f, 0.01940918f,  -0.00231934f,
+        -0.00802612f, 0.04077148f,  -0.14089966f, 0.88064575f,  0.28710938f,  -0.07833862f,
+        0.02011108f,  -0.00244141f, -0.00808716f, 0.04122925f,  -0.14263916f, 0.87374878f,
+        0.29733276f,  -0.08090210f, 0.02084351f,  -0.00253296f, -0.00811768f, 0.04165649f,
+        -0.14428711f, 0.86666870f,  0.30761719f,  -0.08343506f, 0.02154541f,  -0.00265503f,
+        -0.00814819f, 0.04205322f,  -0.14578247f, 0.85940552f,  0.31793213f,  -0.08596802f,
+        0.02227783f,  -0.00277710f, -0.00814819f, 0.04238892f,  -0.14715576f, 0.85202026f,
+        0.32833862f,  -0.08847046f, 0.02297974f,  -0.00289917f, -0.00817871f, 0.04272461f,
+        -0.14840698f, 0.84445190f,  0.33874512f,  -0.09097290f, 0.02371216f,  -0.00302124f,
+        -0.00817871f, 0.04299927f,  -0.14953613f, 0.83673096f,  0.34924316f,  -0.09347534f,
+        0.02441406f,  -0.00314331f, -0.00817871f, 0.04321289f,  -0.15054321f, 0.82888794f,
+        0.35977173f,  -0.09594727f, 0.02514648f,  -0.00326538f, -0.00814819f, 0.04342651f,
+        -0.15142822f, 0.82086182f,  0.37033081f,  -0.09838867f, 0.02584839f,  -0.00341797f,
+        -0.00814819f, 0.04357910f,  -0.15219116f, 0.81271362f,  0.38092041f,  -0.10079956f,
+        0.02655029f,  -0.00354004f, -0.00811768f, 0.04373169f,  -0.15283203f, 0.80441284f,
+        0.39154053f,  -0.10321045f, 0.02725220f,  -0.00366211f, -0.00808716f, 0.04382324f,
+        -0.15338135f, 0.79598999f,  0.40219116f,  -0.10559082f, 0.02795410f,  -0.00381470f,
+        -0.00805664f, 0.04388428f,  -0.15377808f, 0.78741455f,  0.41287231f,  -0.10794067f,
+        0.02865601f,  -0.00393677f, -0.00799561f, 0.04388428f,  -0.15408325f, 0.77871704f,
+        0.42358398f,  -0.11026001f, 0.02935791f,  -0.00405884f, -0.00793457f, 0.04388428f,
+        -0.15426636f, 0.76989746f,  0.43429565f,  -0.11251831f, 0.03002930f,  -0.00421143f,
+        -0.00787354f, 0.04385376f,  -0.15435791f, 0.76095581f,  0.44500732f,  -0.11477661f,
+        0.03070068f,  -0.00433350f, -0.00781250f, 0.04379272f,  -0.15435791f, 0.75192261f,
+        0.45574951f,  -0.11697388f, 0.03137207f,  -0.00448608f, -0.00775146f, 0.04367065f,
+        -0.15420532f, 0.74273682f,  0.46649170f,  -0.11914062f, 0.03201294f,  -0.00460815f,
+        -0.00769043f, 0.04354858f,  -0.15399170f, 0.73345947f,  0.47723389f,  -0.12127686f,
+        0.03268433f,  -0.00473022f, -0.00759888f, 0.04339600f,  -0.15365601f, 0.72406006f,
+        0.48794556f,  -0.12335205f, 0.03329468f,  -0.00488281f, -0.00750732f, 0.04321289f,
+        -0.15322876f, 0.71456909f,  0.49868774f,  -0.12539673f, 0.03393555f,  -0.00500488f,
+        -0.00741577f, 0.04296875f,  -0.15270996f, 0.70498657f,  0.50936890f,  -0.12738037f,
+        0.03454590f,  -0.00515747f, -0.00732422f, 0.04272461f,  -0.15209961f, 0.69528198f,
+        0.52008057f,  -0.12930298f, 0.03515625f,  -0.00527954f, -0.00723267f, 0.04248047f,
+        -0.15136719f, 0.68551636f,  0.53076172f,  -0.13119507f, 0.03573608f,  -0.00543213f,
+        -0.00714111f, 0.04217529f,  -0.15057373f, 0.67565918f,  0.54138184f,  -0.13299561f,
+        0.03631592f,  -0.00555420f, -0.00701904f, 0.04183960f,  -0.14968872f, 0.66571045f,
+        0.55200195f,  -0.13476562f, 0.03689575f,  -0.00567627f, -0.00692749f, 0.04150391f,
+        -0.14871216f, 0.65567017f,  0.56259155f,  -0.13647461f, 0.03741455f,  -0.00582886f,
+        -0.00680542f, 0.04113770f,  -0.14767456f, 0.64556885f,  0.57315063f,  -0.13812256f,
+        0.03796387f,  -0.00595093f, -0.00668335f, 0.04074097f,  -0.14651489f, 0.63540649f,
+        0.58364868f,  -0.13970947f, 0.03845215f,  -0.00607300f, -0.00656128f, 0.04031372f,
+        -0.14529419f, 0.62518311f,  0.59411621f,  -0.14120483f, 0.03897095f,  -0.00619507f,
+        -0.00643921f, 0.03988647f,  -0.14401245f, 0.61486816f,  0.60452271f,  -0.14263916f,
+        0.03942871f,  -0.00631714f, -0.00631714f, 0.03942871f,  -0.14263916f, 0.60452271f,
+        0.61486816f,  -0.14401245f, 0.03988647f,  -0.00643921f, -0.00619507f, 0.03897095f,
+        -0.14120483f, 0.59411621f,  0.62518311f,  -0.14529419f, 0.04031372f,  -0.00656128f,
+        -0.00607300f, 0.03845215f,  -0.13970947f, 0.58364868f,  0.63540649f,  -0.14651489f,
+        0.04074097f,  -0.00668335f, -0.00595093f, 0.03796387f,  -0.13812256f, 0.57315063f,
+        0.64556885f,  -0.14767456f, 0.04113770f,  -0.00680542f, -0.00582886f, 0.03741455f,
+        -0.13647461f, 0.56259155f,  0.65567017f,  -0.14871216f, 0.04150391f,  -0.00692749f,
+        -0.00567627f, 0.03689575f,  -0.13476562f, 0.55200195f,  0.66571045f,  -0.14968872f,
+        0.04183960f,  -0.00701904f, -0.00555420f, 0.03631592f,  -0.13299561f, 0.54138184f,
+        0.67565918f,  -0.15057373f, 0.04217529f,  -0.00714111f, -0.00543213f, 0.03573608f,
+        -0.13119507f, 0.53076172f,  0.68551636f,  -0.15136719f, 0.04248047f,  -0.00723267f,
+        -0.00527954f, 0.03515625f,  -0.12930298f, 0.52008057f,  0.69528198f,  -0.15209961f,
+        0.04272461f,  -0.00732422f, -0.00515747f, 0.03454590f,  -0.12738037f, 0.50936890f,
+        0.70498657f,  -0.15270996f, 0.04296875f,  -0.00741577f, -0.00500488f, 0.03393555f,
+        -0.12539673f, 0.49868774f,  0.71456909f,  -0.15322876f, 0.04321289f,  -0.00750732f,
+        -0.00488281f, 0.03329468f,  -0.12335205f, 0.48794556f,  0.72406006f,  -0.15365601f,
+        0.04339600f,  -0.00759888f, -0.00473022f, 0.03268433f,  -0.12127686f, 0.47723389f,
+        0.73345947f,  -0.15399170f, 0.04354858f,  -0.00769043f, -0.00460815f, 0.03201294f,
+        -0.11914062f, 0.46649170f,  0.74273682f,  -0.15420532f, 0.04367065f,  -0.00775146f,
+        -0.00448608f, 0.03137207f,  -0.11697388f, 0.45574951f,  0.75192261f,  -0.15435791f,
+        0.04379272f,  -0.00781250f, -0.00433350f, 0.03070068f,  -0.11477661f, 0.44500732f,
+        0.76095581f,  -0.15435791f, 0.04385376f,  -0.00787354f, -0.00421143f, 0.03002930f,
+        -0.11251831f, 0.43429565f,  0.76989746f,  -0.15426636f, 0.04388428f,  -0.00793457f,
+        -0.00405884f, 0.02935791f,  -0.11026001f, 0.42358398f,  0.77871704f,  -0.15408325f,
+        0.04388428f,  -0.00799561f, -0.00393677f, 0.02865601f,  -0.10794067f, 0.41287231f,
+        0.78741455f,  -0.15377808f, 0.04388428f,  -0.00805664f, -0.00381470f, 0.02795410f,
+        -0.10559082f, 0.40219116f,  0.79598999f,  -0.15338135f, 0.04382324f,  -0.00808716f,
+        -0.00366211f, 0.02725220f,  -0.10321045f, 0.39154053f,  0.80441284f,  -0.15283203f,
+        0.04373169f,  -0.00811768f, -0.00354004f, 0.02655029f,  -0.10079956f, 0.38092041f,
+        0.81271362f,  -0.15219116f, 0.04357910f,  -0.00814819f, -0.00341797f, 0.02584839f,
+        -0.09838867f, 0.37033081f,  0.82086182f,  -0.15142822f, 0.04342651f,  -0.00814819f,
+        -0.00326538f, 0.02514648f,  -0.09594727f, 0.35977173f,  0.82888794f,  -0.15054321f,
+        0.04321289f,  -0.00817871f, -0.00314331f, 0.02441406f,  -0.09347534f, 0.34924316f,
+        0.83673096f,  -0.14953613f, 0.04299927f,  -0.00817871f, -0.00302124f, 0.02371216f,
+        -0.09097290f, 0.33874512f,  0.84445190f,  -0.14840698f, 0.04272461f,  -0.00817871f,
+        -0.00289917f, 0.02297974f,  -0.08847046f, 0.32833862f,  0.85202026f,  -0.14715576f,
+        0.04238892f,  -0.00814819f, -0.00277710f, 0.02227783f,  -0.08596802f, 0.31793213f,
+        0.85940552f,  -0.14578247f, 0.04205322f,  -0.00814819f, -0.00265503f, 0.02154541f,
+        -0.08343506f, 0.30761719f,  0.86666870f,  -0.14428711f, 0.04165649f,  -0.00811768f,
+        -0.00253296f, 0.02084351f,  -0.08090210f, 0.29733276f,  0.87374878f,  -0.14263916f,
+        0.04122925f,  -0.00808716f, -0.00244141f, 0.02011108f,  -0.07833862f, 0.28710938f,
+        0.88064575f,  -0.14089966f, 0.04077148f,  -0.00802612f, -0.00231934f, 0.01940918f,
+        -0.07577515f, 0.27694702f,  0.88739014f,  -0.13900757f, 0.04025269f,  -0.00796509f,
+        -0.00219727f, 0.01870728f,  -0.07321167f, 0.26687622f,  0.89395142f,  -0.13696289f,
+        0.03970337f,  -0.00790405f, -0.00210571f, 0.01797485f,  -0.07064819f, 0.25683594f,
+        0.90036011f,  -0.13479614f, 0.03909302f,  -0.00784302f, -0.00198364f, 0.01727295f,
+        -0.06808472f, 0.24691772f,  0.90658569f,  -0.13250732f, 0.03848267f,  -0.00775146f,
+        -0.00189209f, 0.01657104f,  -0.06555176f, 0.23703003f,  0.91262817f,  -0.13006592f,
+        0.03781128f,  -0.00765991f, -0.00177002f, 0.01586914f,  -0.06298828f, 0.22723389f,
+        0.91848755f,  -0.12750244f, 0.03707886f,  -0.00753784f, -0.00167847f, 0.01519775f,
+        -0.06042480f, 0.21752930f,  0.92413330f,  -0.12478638f, 0.03631592f,  -0.00744629f,
+        -0.00158691f, 0.01449585f,  -0.05786133f, 0.20791626f,  0.92962646f,  -0.12194824f,
+        0.03552246f,  -0.00732422f, -0.00149536f, 0.01382446f,  -0.05532837f, 0.19839478f,
+        0.93490601f,  -0.11895752f, 0.03466797f,  -0.00717163f, -0.00140381f, 0.01315308f,
+        -0.05279541f, 0.18893433f,  0.94000244f,  -0.11584473f, 0.03378296f,  -0.00701904f,
+        -0.00131226f, 0.01248169f,  -0.05029297f, 0.17959595f,  0.94491577f,  -0.11254883f,
+        0.03286743f,  -0.00686646f, -0.00122070f, 0.01181030f,  -0.04779053f, 0.17034912f,
+        0.94961548f,  -0.10916138f, 0.03189087f,  -0.00671387f, -0.00112915f, 0.01113892f,
+        -0.04528809f, 0.16119385f,  0.95413208f,  -0.10559082f, 0.03085327f,  -0.00653076f,
+        -0.00106812f, 0.01049805f,  -0.04281616f, 0.15213013f,  0.95843506f,  -0.10189819f,
+        0.02981567f,  -0.00631714f, -0.00097656f, 0.00985718f,  -0.04034424f, 0.14318848f,
+        0.96252441f,  -0.09805298f, 0.02868652f,  -0.00613403f, -0.00091553f, 0.00924683f,
+        -0.03790283f, 0.13436890f,  0.96643066f,  -0.09405518f, 0.02755737f,  -0.00592041f,
+        -0.00082397f, 0.00860596f,  -0.03549194f, 0.12561035f,  0.97012329f,  -0.08993530f,
+        0.02636719f,  -0.00567627f, -0.00076294f, 0.00799561f,  -0.03308105f, 0.11700439f,
+        0.97360229f,  -0.08566284f, 0.02511597f,  -0.00543213f, -0.00070190f, 0.00738525f,
+        -0.03073120f, 0.10848999f,  0.97686768f,  -0.08123779f, 0.02383423f,  -0.00518799f,
+        -0.00061035f, 0.00680542f,  -0.02835083f, 0.10012817f,  0.97991943f,  -0.07666016f,
+        0.02252197f,  -0.00494385f, -0.00054932f, 0.00622559f,  -0.02603149f, 0.09185791f,
+        0.98278809f,  -0.07192993f, 0.02114868f,  -0.00463867f, -0.00048828f, 0.00564575f,
+        -0.02374268f, 0.08370972f,  0.98541260f,  -0.06707764f, 0.01971436f,  -0.00436401f,
+        -0.00042725f, 0.00506592f,  -0.02145386f, 0.07568359f,  0.98782349f,  -0.06207275f,
+        0.01828003f,  -0.00405884f, -0.00039673f, 0.00451660f,  -0.01922607f, 0.06777954f,
+        0.99002075f,  -0.05691528f, 0.01678467f,  -0.00375366f, -0.00033569f, 0.00396729f,
+        -0.01699829f, 0.05999756f,  0.99200439f,  -0.05163574f, 0.01522827f,  -0.00341797f,
+        -0.00027466f, 0.00344849f,  -0.01483154f, 0.05233765f,  0.99377441f,  -0.04620361f,
+        0.01364136f,  -0.00308228f, -0.00024414f, 0.00292969f,  -0.01266479f, 0.04483032f,
+        0.99533081f,  -0.04061890f, 0.01202393f,  -0.00274658f, -0.00018311f, 0.00241089f,
+        -0.01055908f, 0.03741455f,  0.99664307f,  -0.03488159f, 0.01037598f,  -0.00238037f,
+        -0.00015259f, 0.00192261f,  -0.00845337f, 0.03018188f,  0.99774170f,  -0.02899170f,
+        0.00866699f,  -0.00201416f, -0.00009155f, 0.00143433f,  -0.00640869f, 0.02304077f,
+        0.99862671f,  -0.02297974f, 0.00689697f,  -0.00161743f, -0.00006104f, 0.00097656f,
+        -0.00439453f, 0.01605225f,  0.99929810f,  -0.01684570f, 0.00512695f,  -0.00122070f,
+        -0.00003052f, 0.00051880f,  -0.00241089f, 0.00918579f,  0.99975586f,  -0.01052856f,
+        0.00329590f,  -0.00079346f, 0.00000000f,  0.00006104f,  -0.00048828f, 0.00247192f,
+        0.99996948f,  -0.00408936f, 0.00143433f,  -0.00036621f,
+    };
+
+    const auto get_lut = [&]() -> std::span<const f32> {
+        if (sample_rate_ratio <= 1.0f) {
+            return std::span<const f32>(lut2.data(), lut2.size());
+        } else if (sample_rate_ratio < 1.3f) {
+            return std::span<const f32>(lut1.data(), lut1.size());
+        } else {
+            return std::span<const f32>(lut0.data(), lut0.size());
+        }
+    };
+
+    auto lut{get_lut()};
+    u32 read_index{0};
+    for (u32 i = 0; i < samples_to_write; i++) {
+        const auto lut_index{(fraction.get_frac() >> 8) * 8};
+        const Common::FixedPoint<56, 8> sample0{input[read_index + 0] * lut[lut_index + 0]};
+        const Common::FixedPoint<56, 8> sample1{input[read_index + 1] * lut[lut_index + 1]};
+        const Common::FixedPoint<56, 8> sample2{input[read_index + 2] * lut[lut_index + 2]};
+        const Common::FixedPoint<56, 8> sample3{input[read_index + 3] * lut[lut_index + 3]};
+        const Common::FixedPoint<56, 8> sample4{input[read_index + 4] * lut[lut_index + 4]};
+        const Common::FixedPoint<56, 8> sample5{input[read_index + 5] * lut[lut_index + 5]};
+        const Common::FixedPoint<56, 8> sample6{input[read_index + 6] * lut[lut_index + 6]};
+        const Common::FixedPoint<56, 8> sample7{input[read_index + 7] * lut[lut_index + 7]};
+        output[i] = (sample0 + sample1 + sample2 + sample3 + sample4 + sample5 + sample6 + sample7)
+                        .to_int_floor();
+        fraction += sample_rate_ratio;
+        read_index += static_cast<u32>(fraction.to_int_floor());
+        fraction.clear_int();
+    }
+}
+
+void Resample(std::span<s32> output, std::span<const s16> input,
+              const Common::FixedPoint<49, 15>& sample_rate_ratio,
+              Common::FixedPoint<49, 15>& fraction, const u32 samples_to_write,
+              const SrcQuality src_quality) {
+
+    switch (src_quality) {
+    case SrcQuality::Low:
+        ResampleLowQuality(output, input, sample_rate_ratio, fraction, samples_to_write);
+        break;
+    case SrcQuality::Medium:
+        ResampleNormalQuality(output, input, sample_rate_ratio, fraction, samples_to_write);
+        break;
+    case SrcQuality::High:
+        ResampleHighQuality(output, input, sample_rate_ratio, fraction, samples_to_write);
+        break;
+    }
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/resample/resample.h b/src/audio_core/renderer/command/resample/resample.h
new file mode 100644
index 0000000000..ba9209b82c
--- /dev/null
+++ b/src/audio_core/renderer/command/resample/resample.h
@@ -0,0 +1,29 @@
+// 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"
+#include "common/fixed_point.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * Resample an input buffer into an output buffer, according to the sample_rate_ratio.
+ *
+ * @param output            - Output buffer.
+ * @param input             - Input buffer.
+ * @param sample_rate_ratio - Ratio for resampling.
+                              e.g 32000/48000 = 0.666 input samples read per output.
+ * @param fraction          - Current read fraction, written to and should be passed back in for
+ *                            multiple calls.
+ * @param samples_to_write  - Number of samples to write.
+ * @param src_quality       - Resampling quality.
+ */
+void Resample(std::span<s32> output, std::span<const s16> input,
+              const Common::FixedPoint<49, 15>& sample_rate_ratio,
+              Common::FixedPoint<49, 15>& fraction, u32 samples_to_write, SrcQuality src_quality);
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/resample/upsample.cpp b/src/audio_core/renderer/command/resample/upsample.cpp
new file mode 100644
index 0000000000..6c3ff31f7d
--- /dev/null
+++ b/src/audio_core/renderer/command/resample/upsample.cpp
@@ -0,0 +1,262 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <array>
+
+#include "audio_core/renderer/adsp/command_list_processor.h"
+#include "audio_core/renderer/command/resample/upsample.h"
+#include "audio_core/renderer/upsampler/upsampler_info.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * Upsampling impl. Input must be 8K, 16K or 32K, output is 48K.
+ *
+ * @param output              - Output buffer.
+ * @param input               - Input buffer.
+ * @param target_sample_count - Number of samples for output.
+ * @param state               - Upsampler state, updated each call.
+ */
+static void SrcProcessFrame(std::span<s32> output, std::span<const s32> input,
+                            const u32 target_sample_count, const u32 source_sample_count,
+                            UpsamplerState* state) {
+    constexpr u32 WindowSize = 10;
+    constexpr std::array<Common::FixedPoint<24, 8>, WindowSize> SincWindow1{
+        51.93359375f, -18.80078125f, 9.73046875f, -5.33203125f, 2.84375f,
+        -1.41015625f, 0.62109375f,   -0.2265625f, 0.0625f,      -0.00390625f,
+    };
+    constexpr std::array<Common::FixedPoint<24, 8>, WindowSize> SincWindow2{
+        105.35546875f, -24.52734375f, 11.9609375f,  -6.515625f, 3.52734375f,
+        -1.796875f,    0.828125f,     -0.32421875f, 0.1015625f, -0.015625f,
+    };
+    constexpr std::array<Common::FixedPoint<24, 8>, WindowSize> SincWindow3{
+        122.08203125f, -16.47656250f, 7.68359375f,  -4.15625000f, 2.26171875f,
+        -1.16796875f,  0.54687500f,   -0.22265625f, 0.07421875f,  -0.01171875f,
+    };
+    constexpr std::array<Common::FixedPoint<24, 8>, WindowSize> SincWindow4{
+        23.73437500f, -9.62109375f, 5.07812500f,  -2.78125000f, 1.46875000f,
+        -0.71484375f, 0.30859375f,  -0.10546875f, 0.02734375f,  0.00000000f,
+    };
+    constexpr std::array<Common::FixedPoint<24, 8>, WindowSize> SincWindow5{
+        80.62500000f, -24.67187500f, 12.44921875f, -6.80859375f, 3.66406250f,
+        -1.83984375f, 0.83203125f,   -0.31640625f, 0.09375000f,  -0.01171875f,
+    };
+
+    if (!state->initialized) {
+        switch (source_sample_count) {
+        case 40:
+            state->window_size = WindowSize;
+            state->ratio = 6.0f;
+            state->history.fill(0);
+            break;
+
+        case 80:
+            state->window_size = WindowSize;
+            state->ratio = 3.0f;
+            state->history.fill(0);
+            break;
+
+        case 160:
+            state->window_size = WindowSize;
+            state->ratio = 1.5f;
+            state->history.fill(0);
+            break;
+
+        default:
+            LOG_ERROR(Service_Audio, "Invalid upsampling source count {}!", source_sample_count);
+            // This continues anyway, but let's assume 160 for sanity
+            state->window_size = WindowSize;
+            state->ratio = 1.5f;
+            state->history.fill(0);
+            break;
+        }
+
+        state->history_input_index = 0;
+        state->history_output_index = 9;
+        state->history_start_index = 0;
+        state->history_end_index = UpsamplerState::HistorySize - 1;
+        state->initialized = true;
+    }
+
+    if (target_sample_count == 0) {
+        return;
+    }
+
+    u32 read_index{0};
+
+    auto increment = [&]() -> void {
+        state->history[state->history_input_index] = input[read_index++];
+        state->history_input_index =
+            static_cast<u16>((state->history_input_index + 1) % UpsamplerState::HistorySize);
+        state->history_output_index =
+            static_cast<u16>((state->history_output_index + 1) % UpsamplerState::HistorySize);
+    };
+
+    auto calculate_sample = [&state](std::span<const Common::FixedPoint<24, 8>> coeffs1,
+                                     std::span<const Common::FixedPoint<24, 8>> coeffs2) -> s32 {
+        auto output_index{state->history_output_index};
+        auto start_pos{output_index - state->history_start_index + 1U};
+        auto end_pos{10U};
+
+        if (start_pos < 10) {
+            end_pos = start_pos;
+        }
+
+        u64 prev_contrib{0};
+        u32 coeff_index{0};
+        for (; coeff_index < end_pos; coeff_index++, output_index--) {
+            prev_contrib += static_cast<u64>(state->history[output_index].to_raw()) *
+                            coeffs1[coeff_index].to_raw();
+        }
+
+        auto end_index{state->history_end_index};
+        for (; start_pos < 9; start_pos++, coeff_index++, end_index--) {
+            prev_contrib += static_cast<u64>(state->history[end_index].to_raw()) *
+                            coeffs1[coeff_index].to_raw();
+        }
+
+        output_index =
+            static_cast<u16>((state->history_output_index + 1) % UpsamplerState::HistorySize);
+        start_pos = state->history_end_index - output_index + 1U;
+        end_pos = 10U;
+
+        if (start_pos < 10) {
+            end_pos = start_pos;
+        }
+
+        u64 next_contrib{0};
+        coeff_index = 0;
+        for (; coeff_index < end_pos; coeff_index++, output_index++) {
+            next_contrib += static_cast<u64>(state->history[output_index].to_raw()) *
+                            coeffs2[coeff_index].to_raw();
+        }
+
+        auto start_index{state->history_start_index};
+        for (; start_pos < 9; start_pos++, start_index++, coeff_index++) {
+            next_contrib += static_cast<u64>(state->history[start_index].to_raw()) *
+                            coeffs2[coeff_index].to_raw();
+        }
+
+        return static_cast<s32>(((prev_contrib >> 15) + (next_contrib >> 15)) >> 8);
+    };
+
+    switch (state->ratio.to_int_floor()) {
+    // 40 -> 240
+    case 6:
+        for (u32 write_index = 0; write_index < target_sample_count; write_index++) {
+            switch (state->sample_index) {
+            case 0:
+                increment();
+                output[write_index] = state->history[state->history_output_index].to_int_floor();
+                break;
+
+            case 1:
+                output[write_index] = calculate_sample(SincWindow3, SincWindow4);
+                break;
+
+            case 2:
+                output[write_index] = calculate_sample(SincWindow2, SincWindow1);
+                break;
+
+            case 3:
+                output[write_index] = calculate_sample(SincWindow5, SincWindow5);
+                break;
+
+            case 4:
+                output[write_index] = calculate_sample(SincWindow1, SincWindow2);
+                break;
+
+            case 5:
+                output[write_index] = calculate_sample(SincWindow4, SincWindow3);
+                break;
+            }
+            state->sample_index = static_cast<u8>((state->sample_index + 1) % 6);
+        }
+        break;
+
+    // 80 -> 240
+    case 3:
+        for (u32 write_index = 0; write_index < target_sample_count; write_index++) {
+            switch (state->sample_index) {
+            case 0:
+                increment();
+                output[write_index] = state->history[state->history_output_index].to_int_floor();
+                break;
+
+            case 1:
+                output[write_index] = calculate_sample(SincWindow2, SincWindow1);
+                break;
+
+            case 2:
+                output[write_index] = calculate_sample(SincWindow1, SincWindow2);
+                break;
+            }
+            state->sample_index = static_cast<u8>((state->sample_index + 1) % 3);
+        }
+        break;
+
+    // 160 -> 240
+    default:
+        for (u32 write_index = 0; write_index < target_sample_count; write_index++) {
+            switch (state->sample_index) {
+            case 0:
+                increment();
+                output[write_index] = state->history[state->history_output_index].to_int_floor();
+                break;
+
+            case 1:
+                output[write_index] = calculate_sample(SincWindow1, SincWindow2);
+                break;
+
+            case 2:
+                increment();
+                output[write_index] = calculate_sample(SincWindow2, SincWindow1);
+                break;
+            }
+            state->sample_index = static_cast<u8>((state->sample_index + 1) % 3);
+        }
+
+        break;
+    }
+}
+
+auto UpsampleCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor,
+                           std::string& string) -> void {
+    string += fmt::format("UpsampleCommand\n\tsource_sample_count {} source_sample_rate {}",
+                          source_sample_count, source_sample_rate);
+    const auto upsampler{reinterpret_cast<UpsamplerInfo*>(upsampler_info)};
+    if (upsampler != nullptr) {
+        string += fmt::format("\n\tUpsampler\n\t\tenabled {} sample count {}\n\tinputs: ",
+                              upsampler->enabled, upsampler->sample_count);
+        for (u32 i = 0; i < upsampler->input_count; i++) {
+            string += fmt::format("{:02X}, ", upsampler->inputs[i]);
+        }
+    }
+    string += "\n";
+}
+
+void UpsampleCommand::Process(const ADSP::CommandListProcessor& processor) {
+    const auto info{reinterpret_cast<UpsamplerInfo*>(upsampler_info)};
+    const auto input_count{std::min(info->input_count, buffer_count)};
+    const std::span<const s16> inputs_{reinterpret_cast<const s16*>(inputs), input_count};
+
+    for (u32 i = 0; i < input_count; i++) {
+        const auto channel{inputs_[i]};
+
+        if (channel >= 0 && channel < static_cast<s16>(processor.buffer_count)) {
+            auto state{&info->states[i]};
+            std::span<s32> output{
+                reinterpret_cast<s32*>(samples_buffer + info->sample_count * channel * sizeof(s32)),
+                info->sample_count};
+            auto input{processor.mix_buffers.subspan(channel * processor.sample_count,
+                                                     processor.sample_count)};
+
+            SrcProcessFrame(output, input, info->sample_count, source_sample_count, state);
+        }
+    }
+}
+
+bool UpsampleCommand::Verify(const ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/resample/upsample.h b/src/audio_core/renderer/command/resample/upsample.h
new file mode 100644
index 0000000000..bfc94e8aff
--- /dev/null
+++ b/src/audio_core/renderer/command/resample/upsample.h
@@ -0,0 +1,60 @@
+// 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::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * AudioRenderer command for upsampling a mix buffer to 48Khz.
+ * Input must be 8Khz, 16Khz or 32Khz, and output will be 48Khz.
+ */
+struct UpsampleCommand : 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::CommandListProcessor& processor) override;
+
+    /// Pointer to the output samples buffer.
+    CpuAddr samples_buffer;
+    /// Pointer to input mix buffer indexes.
+    CpuAddr inputs;
+    /// Number of input mix buffers.
+    u32 buffer_count;
+    /// Unknown, unused.
+    u32 unk_20;
+    /// Source data sample count.
+    u32 source_sample_count;
+    /// Source data sample rate.
+    u32 source_sample_rate;
+    /// Pointer to the upsampler info for this command.
+    CpuAddr upsampler_info;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/sink/circular_buffer.cpp b/src/audio_core/renderer/command/sink/circular_buffer.cpp
new file mode 100644
index 0000000000..ded5afc94f
--- /dev/null
+++ b/src/audio_core/renderer/command/sink/circular_buffer.cpp
@@ -0,0 +1,48 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <vector>
+
+#include "audio_core/renderer/adsp/command_list_processor.h"
+#include "audio_core/renderer/command/sink/circular_buffer.h"
+#include "core/memory.h"
+
+namespace AudioCore::AudioRenderer {
+
+void CircularBufferSinkCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor,
+                                     std::string& string) {
+    string += fmt::format(
+        "CircularBufferSinkCommand\n\tinput_count {} ring size {:04X} ring pos {:04X}\n\tinputs: ",
+        input_count, size, pos);
+    for (u32 i = 0; i < input_count; i++) {
+        string += fmt::format("{:02X}, ", inputs[i]);
+    }
+    string += "\n";
+}
+
+void CircularBufferSinkCommand::Process(const ADSP::CommandListProcessor& processor) {
+    constexpr s32 min{std::numeric_limits<s16>::min()};
+    constexpr s32 max{std::numeric_limits<s16>::max()};
+
+    std::vector<s16> output(processor.sample_count);
+    for (u32 channel = 0; channel < input_count; channel++) {
+        auto input{processor.mix_buffers.subspan(inputs[channel] * processor.sample_count,
+                                                 processor.sample_count)};
+        for (u32 sample_index = 0; sample_index < processor.sample_count; sample_index++) {
+            output[sample_index] = static_cast<s16>(std::clamp(input[sample_index], min, max));
+        }
+
+        processor.memory->WriteBlockUnsafe(address + pos, output.data(),
+                                           output.size() * sizeof(s16));
+        pos += static_cast<u32>(processor.sample_count * sizeof(s16));
+        if (pos >= size) {
+            pos = 0;
+        }
+    }
+}
+
+bool CircularBufferSinkCommand::Verify(const ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/sink/circular_buffer.h b/src/audio_core/renderer/command/sink/circular_buffer.h
new file mode 100644
index 0000000000..e7d5be26e5
--- /dev/null
+++ b/src/audio_core/renderer/command/sink/circular_buffer.h
@@ -0,0 +1,55 @@
+// 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::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * AudioRenderer command for sinking samples to a circular buffer.
+ */
+struct CircularBufferSinkCommand : 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::CommandListProcessor& processor) override;
+
+    /// Number of input mix buffers
+    u32 input_count;
+    /// Input mix buffer indexes
+    std::array<s16, MaxChannels> inputs;
+    /// Circular buffer address
+    CpuAddr address;
+    /// Circular buffer size
+    u32 size;
+    /// Current buffer offset
+    u32 pos;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/sink/device.cpp b/src/audio_core/renderer/command/sink/device.cpp
new file mode 100644
index 0000000000..47e0c67226
--- /dev/null
+++ b/src/audio_core/renderer/command/sink/device.cpp
@@ -0,0 +1,55 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <algorithm>
+
+#include "audio_core/renderer/adsp/command_list_processor.h"
+#include "audio_core/renderer/command/sink/device.h"
+#include "audio_core/sink/sink.h"
+
+namespace AudioCore::AudioRenderer {
+
+void DeviceSinkCommand::Dump([[maybe_unused]] const ADSP::CommandListProcessor& processor,
+                             std::string& string) {
+    string += fmt::format("DeviceSinkCommand\n\t{} session {} input_count {}\n\tinputs: ",
+                          std::string_view(name), session_id, input_count);
+    for (u32 i = 0; i < input_count; i++) {
+        string += fmt::format("{:02X}, ", inputs[i]);
+    }
+    string += "\n";
+}
+
+void DeviceSinkCommand::Process(const ADSP::CommandListProcessor& processor) {
+    constexpr s32 min = std::numeric_limits<s16>::min();
+    constexpr s32 max = std::numeric_limits<s16>::max();
+
+    auto stream{processor.GetOutputSinkStream()};
+    stream->SetSystemChannels(input_count);
+
+    Sink::SinkBuffer out_buffer{
+        .frames{TargetSampleCount},
+        .frames_played{0},
+        .tag{0},
+        .consumed{false},
+    };
+
+    std::vector<s16> samples(out_buffer.frames * input_count);
+
+    for (u32 channel = 0; channel < input_count; channel++) {
+        const auto offset{inputs[channel] * out_buffer.frames};
+
+        for (u32 index = 0; index < out_buffer.frames; index++) {
+            samples[index * input_count + channel] =
+                static_cast<s16>(std::clamp(sample_buffer[offset + index], min, max));
+        }
+    }
+
+    out_buffer.tag = reinterpret_cast<u64>(samples.data());
+    stream->AppendBuffer(out_buffer, samples);
+}
+
+bool DeviceSinkCommand::Verify(const ADSP::CommandListProcessor& processor) {
+    return true;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/command/sink/device.h b/src/audio_core/renderer/command/sink/device.h
new file mode 100644
index 0000000000..1099bcf8c8
--- /dev/null
+++ b/src/audio_core/renderer/command/sink/device.h
@@ -0,0 +1,57 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+#include <span>
+#include <string>
+
+#include "audio_core/renderer/command/icommand.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+namespace ADSP {
+class CommandListProcessor;
+}
+
+/**
+ * AudioRenderer command for sinking samples to an output device.
+ */
+struct DeviceSinkCommand : 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 ADSP::CommandListProcessor& processor, std::string& string) override;
+
+    /**
+     * Process this command.
+     *
+     * @param processor - The CommandListProcessor processing this command.
+     */
+    void Process(const ADSP::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 ADSP::CommandListProcessor& processor) override;
+
+    /// Device name
+    char name[0x100];
+    /// System session id (unused)
+    s32 session_id;
+    /// Sample buffer to sink
+    std::span<s32> sample_buffer;
+    /// Number of input channels
+    u32 input_count;
+    /// Mix buffer indexes for each channel
+    std::array<s16, MaxChannels> inputs;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/effect/aux_.cpp b/src/audio_core/renderer/effect/aux_.cpp
new file mode 100644
index 0000000000..51e780ef1f
--- /dev/null
+++ b/src/audio_core/renderer/effect/aux_.cpp
@@ -0,0 +1,93 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/effect/aux_.h"
+
+namespace AudioCore::AudioRenderer {
+
+void AuxInfo::Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params,
+                     const PoolMapper& pool_mapper) {
+    auto in_specific{reinterpret_cast<const ParameterVersion1*>(in_params.specific.data())};
+    auto params{reinterpret_cast<ParameterVersion1*>(parameter.data())};
+
+    std::memcpy(params, in_specific, sizeof(ParameterVersion1));
+    mix_id = in_params.mix_id;
+    process_order = in_params.process_order;
+    enabled = in_params.enabled;
+    if (buffer_unmapped || in_params.is_new) {
+        const bool send_unmapped{!pool_mapper.TryAttachBuffer(
+            error_info, workbuffers[0], in_specific->send_buffer_info_address,
+            sizeof(AuxBufferInfo) + in_specific->count_max * sizeof(s32))};
+        const bool return_unmapped{!pool_mapper.TryAttachBuffer(
+            error_info, workbuffers[1], in_specific->return_buffer_info_address,
+            sizeof(AuxBufferInfo) + in_specific->count_max * sizeof(s32))};
+
+        buffer_unmapped = send_unmapped || return_unmapped;
+
+        if (!buffer_unmapped) {
+            auto send{workbuffers[0].GetReference(false)};
+            send_buffer_info = send + sizeof(AuxInfoDsp);
+            send_buffer = send + sizeof(AuxBufferInfo);
+
+            auto ret{workbuffers[1].GetReference(false)};
+            return_buffer_info = ret + sizeof(AuxInfoDsp);
+            return_buffer = ret + sizeof(AuxBufferInfo);
+        }
+    } else {
+        error_info.error_code = ResultSuccess;
+        error_info.address = CpuAddr(0);
+    }
+}
+
+void AuxInfo::Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params,
+                     const PoolMapper& pool_mapper) {
+    auto in_specific{reinterpret_cast<const ParameterVersion2*>(in_params.specific.data())};
+    auto params{reinterpret_cast<ParameterVersion2*>(parameter.data())};
+
+    std::memcpy(params, in_specific, sizeof(ParameterVersion2));
+    mix_id = in_params.mix_id;
+    process_order = in_params.process_order;
+    enabled = in_params.enabled;
+
+    if (buffer_unmapped || in_params.is_new) {
+        const bool send_unmapped{!pool_mapper.TryAttachBuffer(
+            error_info, workbuffers[0], params->send_buffer_info_address,
+            sizeof(AuxBufferInfo) + params->count_max * sizeof(s32))};
+        const bool return_unmapped{!pool_mapper.TryAttachBuffer(
+            error_info, workbuffers[1], params->return_buffer_info_address,
+            sizeof(AuxBufferInfo) + params->count_max * sizeof(s32))};
+
+        buffer_unmapped = send_unmapped || return_unmapped;
+
+        if (!buffer_unmapped) {
+            auto send{workbuffers[0].GetReference(false)};
+            send_buffer_info = send + sizeof(AuxInfoDsp);
+            send_buffer = send + sizeof(AuxBufferInfo);
+
+            auto ret{workbuffers[1].GetReference(false)};
+            return_buffer_info = ret + sizeof(AuxInfoDsp);
+            return_buffer = ret + sizeof(AuxBufferInfo);
+        }
+    } else {
+        error_info.error_code = ResultSuccess;
+        error_info.address = CpuAddr(0);
+    }
+}
+
+void AuxInfo::UpdateForCommandGeneration() {
+    if (enabled) {
+        usage_state = UsageState::Enabled;
+    } else {
+        usage_state = UsageState::Disabled;
+    }
+}
+
+void AuxInfo::InitializeResultState(EffectResultState& result_state) {}
+
+void AuxInfo::UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) {}
+
+CpuAddr AuxInfo::GetWorkbuffer(s32 index) {
+    return workbuffers[index].GetReference(true);
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/effect/aux_.h b/src/audio_core/renderer/effect/aux_.h
new file mode 100644
index 0000000000..4d3d9e3d95
--- /dev/null
+++ b/src/audio_core/renderer/effect/aux_.h
@@ -0,0 +1,123 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+
+#include "audio_core/common/common.h"
+#include "audio_core/renderer/effect/effect_info_base.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * Auxiliary Buffer used for Aux commands.
+ * Send and return buffers are available (names from the game's perspective).
+ * Send is read by the host, containing a buffer of samples to be used for whatever purpose.
+ * Return is written by the host, writing a mix buffer back to the game.
+ * This allows the game to use pre-processed samples skipping the other render processing,
+ * and to examine or modify what the audio renderer has generated.
+ */
+class AuxInfo : public EffectInfoBase {
+public:
+    struct ParameterVersion1 {
+        /* 0x00 */ std::array<s8, MaxMixBuffers> inputs;
+        /* 0x18 */ std::array<s8, MaxMixBuffers> outputs;
+        /* 0x30 */ u32 mix_buffer_count;
+        /* 0x34 */ u32 sample_rate;
+        /* 0x38 */ u32 count_max;
+        /* 0x3C */ u32 mix_buffer_count_max;
+        /* 0x40 */ CpuAddr send_buffer_info_address;
+        /* 0x48 */ CpuAddr send_buffer_address;
+        /* 0x50 */ CpuAddr return_buffer_info_address;
+        /* 0x58 */ CpuAddr return_buffer_address;
+        /* 0x60 */ u32 mix_buffer_sample_size;
+        /* 0x64 */ u32 sample_count;
+        /* 0x68 */ u32 mix_buffer_sample_count;
+    };
+    static_assert(sizeof(ParameterVersion1) <= sizeof(EffectInfoBase::InParameterVersion1),
+                  "AuxInfo::ParameterVersion1 has the wrong size!");
+
+    struct ParameterVersion2 {
+        /* 0x00 */ std::array<s8, MaxMixBuffers> inputs;
+        /* 0x18 */ std::array<s8, MaxMixBuffers> outputs;
+        /* 0x30 */ u32 mix_buffer_count;
+        /* 0x34 */ u32 sample_rate;
+        /* 0x38 */ u32 count_max;
+        /* 0x3C */ u32 mix_buffer_count_max;
+        /* 0x40 */ CpuAddr send_buffer_info_address;
+        /* 0x48 */ CpuAddr send_buffer_address;
+        /* 0x50 */ CpuAddr return_buffer_info_address;
+        /* 0x58 */ CpuAddr return_buffer_address;
+        /* 0x60 */ u32 mix_buffer_sample_size;
+        /* 0x64 */ u32 sample_count;
+        /* 0x68 */ u32 mix_buffer_sample_count;
+    };
+    static_assert(sizeof(ParameterVersion2) <= sizeof(EffectInfoBase::InParameterVersion2),
+                  "AuxInfo::ParameterVersion2 has the wrong size!");
+
+    struct AuxInfoDsp {
+        /* 0x00 */ u32 read_offset;
+        /* 0x04 */ u32 write_offset;
+        /* 0x08 */ u32 lost_sample_count;
+        /* 0x0C */ u32 total_sample_count;
+        /* 0x10 */ char unk10[0x30];
+    };
+    static_assert(sizeof(AuxInfoDsp) == 0x40, "AuxInfo::AuxInfoDsp has the wrong size!");
+
+    struct AuxBufferInfo {
+        /* 0x00 */ AuxInfoDsp cpu_info;
+        /* 0x40 */ AuxInfoDsp dsp_info;
+    };
+    static_assert(sizeof(AuxBufferInfo) == 0x80, "AuxInfo::AuxBufferInfo has the wrong size!");
+
+    /**
+     * Update the info with new parameters, version 1.
+     *
+     * @param error_info  - Used to write call result code.
+     * @param in_params   - New parameters to update the info with.
+     * @param pool_mapper - Pool for mapping buffers.
+     */
+    void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params,
+                const PoolMapper& pool_mapper) override;
+
+    /**
+     * Update the info with new parameters, version 2.
+     *
+     * @param error_info  - Used to write call result code.
+     * @param in_params   - New parameters to update the info with.
+     * @param pool_mapper - Pool for mapping buffers.
+     */
+    void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params,
+                const PoolMapper& pool_mapper) override;
+
+    /**
+     * Update the info after command generation. Usually only changes its state.
+     */
+    void UpdateForCommandGeneration() override;
+
+    /**
+     * Initialize a new result state. Version 2 only, unused.
+     *
+     * @param result_state - Result state to initialize.
+     */
+    void InitializeResultState(EffectResultState& result_state) override;
+
+    /**
+     * Update the host-side state with the ADSP-side state. Version 2 only, unused.
+     *
+     * @param cpu_state - Host-side result state to update.
+     * @param dsp_state - AudioRenderer-side result state to update from.
+     */
+    void UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) override;
+
+    /**
+     * Get a workbuffer assigned to this effect with the given index.
+     *
+     * @param index - Workbuffer index.
+     * @return Address of the buffer.
+     */
+    CpuAddr GetWorkbuffer(s32 index) override;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/effect/biquad_filter.cpp b/src/audio_core/renderer/effect/biquad_filter.cpp
new file mode 100644
index 0000000000..a1efb32312
--- /dev/null
+++ b/src/audio_core/renderer/effect/biquad_filter.cpp
@@ -0,0 +1,52 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/effect/biquad_filter.h"
+
+namespace AudioCore::AudioRenderer {
+
+void BiquadFilterInfo::Update(BehaviorInfo::ErrorInfo& error_info,
+                              const InParameterVersion1& in_params, const PoolMapper& pool_mapper) {
+    auto in_specific{reinterpret_cast<const ParameterVersion1*>(in_params.specific.data())};
+    auto params{reinterpret_cast<ParameterVersion1*>(parameter.data())};
+
+    std::memcpy(params, in_specific, sizeof(ParameterVersion1));
+    mix_id = in_params.mix_id;
+    process_order = in_params.process_order;
+    enabled = in_params.enabled;
+
+    error_info.error_code = ResultSuccess;
+    error_info.address = CpuAddr(0);
+}
+
+void BiquadFilterInfo::Update(BehaviorInfo::ErrorInfo& error_info,
+                              const InParameterVersion2& in_params, const PoolMapper& pool_mapper) {
+    auto in_specific{reinterpret_cast<const ParameterVersion2*>(in_params.specific.data())};
+    auto params{reinterpret_cast<ParameterVersion2*>(parameter.data())};
+
+    std::memcpy(params, in_specific, sizeof(ParameterVersion2));
+    mix_id = in_params.mix_id;
+    process_order = in_params.process_order;
+    enabled = in_params.enabled;
+
+    error_info.error_code = ResultSuccess;
+    error_info.address = CpuAddr(0);
+}
+
+void BiquadFilterInfo::UpdateForCommandGeneration() {
+    if (enabled) {
+        usage_state = UsageState::Enabled;
+    } else {
+        usage_state = UsageState::Disabled;
+    }
+
+    auto params{reinterpret_cast<ParameterVersion1*>(parameter.data())};
+    params->state = ParameterState::Updated;
+}
+
+void BiquadFilterInfo::InitializeResultState(EffectResultState& result_state) {}
+
+void BiquadFilterInfo::UpdateResultState(EffectResultState& cpu_state,
+                                         EffectResultState& dsp_state) {}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/effect/biquad_filter.h b/src/audio_core/renderer/effect/biquad_filter.h
new file mode 100644
index 0000000000..f53fd5babc
--- /dev/null
+++ b/src/audio_core/renderer/effect/biquad_filter.h
@@ -0,0 +1,79 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+
+#include "audio_core/common/common.h"
+#include "audio_core/renderer/effect/effect_info_base.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+
+class BiquadFilterInfo : public EffectInfoBase {
+public:
+    struct ParameterVersion1 {
+        /* 0x00 */ std::array<s8, MaxChannels> inputs;
+        /* 0x06 */ std::array<s8, MaxChannels> outputs;
+        /* 0x0C */ std::array<s16, 3> b;
+        /* 0x12 */ std::array<s16, 2> a;
+        /* 0x16 */ s8 channel_count;
+        /* 0x17 */ ParameterState state;
+    };
+    static_assert(sizeof(ParameterVersion1) <= sizeof(EffectInfoBase::InParameterVersion1),
+                  "BiquadFilterInfo::ParameterVersion1 has the wrong size!");
+
+    struct ParameterVersion2 {
+        /* 0x00 */ std::array<s8, MaxChannels> inputs;
+        /* 0x06 */ std::array<s8, MaxChannels> outputs;
+        /* 0x0C */ std::array<s16, 3> b;
+        /* 0x12 */ std::array<s16, 2> a;
+        /* 0x16 */ s8 channel_count;
+        /* 0x17 */ ParameterState state;
+    };
+    static_assert(sizeof(ParameterVersion2) <= sizeof(EffectInfoBase::InParameterVersion2),
+                  "BiquadFilterInfo::ParameterVersion2 has the wrong size!");
+
+    /**
+     * Update the info with new parameters, version 1.
+     *
+     * @param error_info  - Used to write call result code.
+     * @param in_params   - New parameters to update the info with.
+     * @param pool_mapper - Pool for mapping buffers.
+     */
+    void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params,
+                const PoolMapper& pool_mapper) override;
+
+    /**
+     * Update the info with new parameters, version 2.
+     *
+     * @param error_info  - Used to write call result code.
+     * @param in_params   - New parameters to update the info with.
+     * @param pool_mapper - Pool for mapping buffers.
+     */
+    void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params,
+                const PoolMapper& pool_mapper) override;
+
+    /**
+     * Update the info after command generation. Usually only changes its state.
+     */
+    void UpdateForCommandGeneration() override;
+
+    /**
+     * Initialize a new result state. Version 2 only, unused.
+     *
+     * @param result_state - Result state to initialize.
+     */
+    void InitializeResultState(EffectResultState& result_state) override;
+
+    /**
+     * Update the host-side state with the ADSP-side state. Version 2 only, unused.
+     *
+     * @param cpu_state - Host-side result state to update.
+     * @param dsp_state - AudioRenderer-side result state to update from.
+     */
+    void UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) override;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/effect/buffer_mixer.cpp b/src/audio_core/renderer/effect/buffer_mixer.cpp
new file mode 100644
index 0000000000..9c8877f012
--- /dev/null
+++ b/src/audio_core/renderer/effect/buffer_mixer.cpp
@@ -0,0 +1,49 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/effect/buffer_mixer.h"
+
+namespace AudioCore::AudioRenderer {
+
+void BufferMixerInfo::Update(BehaviorInfo::ErrorInfo& error_info,
+                             const InParameterVersion1& in_params, const PoolMapper& pool_mapper) {
+    auto in_specific{reinterpret_cast<const ParameterVersion1*>(in_params.specific.data())};
+    auto params{reinterpret_cast<ParameterVersion1*>(parameter.data())};
+
+    std::memcpy(params, in_specific, sizeof(ParameterVersion1));
+    mix_id = in_params.mix_id;
+    process_order = in_params.process_order;
+    enabled = in_params.enabled;
+
+    error_info.error_code = ResultSuccess;
+    error_info.address = CpuAddr(0);
+}
+
+void BufferMixerInfo::Update(BehaviorInfo::ErrorInfo& error_info,
+                             const InParameterVersion2& in_params, const PoolMapper& pool_mapper) {
+    auto in_specific{reinterpret_cast<const ParameterVersion2*>(in_params.specific.data())};
+    auto params{reinterpret_cast<ParameterVersion2*>(parameter.data())};
+
+    std::memcpy(params, in_specific, sizeof(ParameterVersion2));
+    mix_id = in_params.mix_id;
+    process_order = in_params.process_order;
+    enabled = in_params.enabled;
+
+    error_info.error_code = ResultSuccess;
+    error_info.address = CpuAddr(0);
+}
+
+void BufferMixerInfo::UpdateForCommandGeneration() {
+    if (enabled) {
+        usage_state = UsageState::Enabled;
+    } else {
+        usage_state = UsageState::Disabled;
+    }
+}
+
+void BufferMixerInfo::InitializeResultState(EffectResultState& result_state) {}
+
+void BufferMixerInfo::UpdateResultState(EffectResultState& cpu_state,
+                                        EffectResultState& dsp_state) {}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/effect/buffer_mixer.h b/src/audio_core/renderer/effect/buffer_mixer.h
new file mode 100644
index 0000000000..23eed4a8be
--- /dev/null
+++ b/src/audio_core/renderer/effect/buffer_mixer.h
@@ -0,0 +1,75 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+
+#include "audio_core/common/common.h"
+#include "audio_core/renderer/effect/effect_info_base.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+
+class BufferMixerInfo : public EffectInfoBase {
+public:
+    struct ParameterVersion1 {
+        /* 0x00 */ std::array<s8, MaxMixBuffers> inputs;
+        /* 0x18 */ std::array<s8, MaxMixBuffers> outputs;
+        /* 0x30 */ std::array<f32, MaxMixBuffers> volumes;
+        /* 0x90 */ u32 mix_count;
+    };
+    static_assert(sizeof(ParameterVersion1) <= sizeof(EffectInfoBase::InParameterVersion1),
+                  "BufferMixerInfo::ParameterVersion1 has the wrong size!");
+
+    struct ParameterVersion2 {
+        /* 0x00 */ std::array<s8, MaxMixBuffers> inputs;
+        /* 0x18 */ std::array<s8, MaxMixBuffers> outputs;
+        /* 0x30 */ std::array<f32, MaxMixBuffers> volumes;
+        /* 0x90 */ u32 mix_count;
+    };
+    static_assert(sizeof(ParameterVersion2) <= sizeof(EffectInfoBase::InParameterVersion2),
+                  "BufferMixerInfo::ParameterVersion2 has the wrong size!");
+
+    /**
+     * Update the info with new parameters, version 1.
+     *
+     * @param error_info  - Used to write call result code.
+     * @param in_params   - New parameters to update the info with.
+     * @param pool_mapper - Pool for mapping buffers.
+     */
+    void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params,
+                const PoolMapper& pool_mapper) override;
+
+    /**
+     * Update the info with new parameters, version 2.
+     *
+     * @param error_info  - Used to write call result code.
+     * @param in_params   - New parameters to update the info with.
+     * @param pool_mapper - Pool for mapping buffers.
+     */
+    void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params,
+                const PoolMapper& pool_mapper) override;
+
+    /**
+     * Update the info after command generation. Usually only changes its state.
+     */
+    void UpdateForCommandGeneration() override;
+
+    /**
+     * Initialize a new result state. Version 2 only, unused.
+     *
+     * @param result_state - Result state to initialize.
+     */
+    void InitializeResultState(EffectResultState& result_state) override;
+
+    /**
+     * Update the host-side state with the ADSP-side state. Version 2 only, unused.
+     *
+     * @param cpu_state - Host-side result state to update.
+     * @param dsp_state - AudioRenderer-side result state to update from.
+     */
+    void UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) override;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/effect/capture.cpp b/src/audio_core/renderer/effect/capture.cpp
new file mode 100644
index 0000000000..3f038efdb3
--- /dev/null
+++ b/src/audio_core/renderer/effect/capture.cpp
@@ -0,0 +1,82 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/effect/aux_.h"
+#include "audio_core/renderer/effect/capture.h"
+
+namespace AudioCore::AudioRenderer {
+
+void CaptureInfo::Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params,
+                         const PoolMapper& pool_mapper) {
+    auto in_specific{
+        reinterpret_cast<const AuxInfo::ParameterVersion1*>(in_params.specific.data())};
+    auto params{reinterpret_cast<AuxInfo::ParameterVersion1*>(parameter.data())};
+
+    std::memcpy(params, in_specific, sizeof(AuxInfo::ParameterVersion1));
+    mix_id = in_params.mix_id;
+    process_order = in_params.process_order;
+    enabled = in_params.enabled;
+    if (buffer_unmapped || in_params.is_new) {
+        buffer_unmapped = !pool_mapper.TryAttachBuffer(
+            error_info, workbuffers[0], in_specific->send_buffer_info_address,
+            in_specific->count_max * sizeof(s32) + sizeof(AuxInfo::AuxBufferInfo));
+
+        if (!buffer_unmapped) {
+            const auto send_address{workbuffers[0].GetReference(false)};
+            send_buffer_info = send_address + sizeof(AuxInfo::AuxInfoDsp);
+            send_buffer = send_address + sizeof(AuxInfo::AuxBufferInfo);
+            return_buffer_info = 0;
+            return_buffer = 0;
+        }
+    } else {
+        error_info.error_code = ResultSuccess;
+        error_info.address = CpuAddr(0);
+    }
+}
+
+void CaptureInfo::Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params,
+                         const PoolMapper& pool_mapper) {
+    auto in_specific{
+        reinterpret_cast<const AuxInfo::ParameterVersion2*>(in_params.specific.data())};
+    auto params{reinterpret_cast<AuxInfo::ParameterVersion2*>(parameter.data())};
+
+    std::memcpy(params, in_specific, sizeof(AuxInfo::ParameterVersion2));
+    mix_id = in_params.mix_id;
+    process_order = in_params.process_order;
+    enabled = in_params.enabled;
+
+    if (buffer_unmapped || in_params.is_new) {
+        buffer_unmapped = !pool_mapper.TryAttachBuffer(
+            error_info, workbuffers[0], params->send_buffer_info_address,
+            params->count_max * sizeof(s32) + sizeof(AuxInfo::AuxBufferInfo));
+
+        if (!buffer_unmapped) {
+            const auto send_address{workbuffers[0].GetReference(false)};
+            send_buffer_info = send_address + sizeof(AuxInfo::AuxInfoDsp);
+            send_buffer = send_address + sizeof(AuxInfo::AuxBufferInfo);
+            return_buffer_info = 0;
+            return_buffer = 0;
+        }
+    } else {
+        error_info.error_code = ResultSuccess;
+        error_info.address = CpuAddr(0);
+    }
+}
+
+void CaptureInfo::UpdateForCommandGeneration() {
+    if (enabled) {
+        usage_state = UsageState::Enabled;
+    } else {
+        usage_state = UsageState::Disabled;
+    }
+}
+
+void CaptureInfo::InitializeResultState(EffectResultState& result_state) {}
+
+void CaptureInfo::UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) {}
+
+CpuAddr CaptureInfo::GetWorkbuffer(s32 index) {
+    return workbuffers[index].GetReference(true);
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/effect/capture.h b/src/audio_core/renderer/effect/capture.h
new file mode 100644
index 0000000000..6fbed8e6be
--- /dev/null
+++ b/src/audio_core/renderer/effect/capture.h
@@ -0,0 +1,65 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+
+#include "audio_core/common/common.h"
+#include "audio_core/renderer/effect/effect_info_base.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+
+class CaptureInfo : public EffectInfoBase {
+public:
+    /**
+     * Update the info with new parameters, version 1.
+     *
+     * @param error_info  - Used to write call result code.
+     * @param in_params   - New parameters to update the info with.
+     * @param pool_mapper - Pool for mapping buffers.
+     */
+    void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params,
+                const PoolMapper& pool_mapper) override;
+
+    /**
+     * Update the info with new parameters, version 2.
+     *
+     * @param error_info  - Used to write call result code.
+     * @param in_params   - New parameters to update the info with.
+     * @param pool_mapper - Pool for mapping buffers.
+     */
+    void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params,
+                const PoolMapper& pool_mapper) override;
+
+    /**
+     * Update the info after command generation. Usually only changes its state.
+     */
+    void UpdateForCommandGeneration() override;
+
+    /**
+     * Initialize a new result state. Version 2 only, unused.
+     *
+     * @param result_state - Result state to initialize.
+     */
+    void InitializeResultState(EffectResultState& result_state) override;
+
+    /**
+     * Update the host-side state with the ADSP-side state. Version 2 only, unused.
+     *
+     * @param cpu_state - Host-side result state to update.
+     * @param dsp_state - AudioRenderer-side result state to update from.
+     */
+    void UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) override;
+
+    /**
+     * Get a workbuffer assigned to this effect with the given index.
+     *
+     * @param index - Workbuffer index.
+     * @return Address of the buffer.
+     */
+    CpuAddr GetWorkbuffer(s32 index) override;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/effect/compressor.cpp b/src/audio_core/renderer/effect/compressor.cpp
new file mode 100644
index 0000000000..220ae02f96
--- /dev/null
+++ b/src/audio_core/renderer/effect/compressor.cpp
@@ -0,0 +1,40 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/effect/compressor.h"
+
+namespace AudioCore::AudioRenderer {
+
+void CompressorInfo::Update(BehaviorInfo::ErrorInfo& error_info,
+                            const InParameterVersion1& in_params, const PoolMapper& pool_mapper) {}
+
+void CompressorInfo::Update(BehaviorInfo::ErrorInfo& error_info,
+                            const InParameterVersion2& in_params, const PoolMapper& pool_mapper) {
+    auto in_specific{reinterpret_cast<const ParameterVersion1*>(in_params.specific.data())};
+    auto params{reinterpret_cast<ParameterVersion1*>(parameter.data())};
+
+    std::memcpy(params, in_specific, sizeof(ParameterVersion1));
+    mix_id = in_params.mix_id;
+    process_order = in_params.process_order;
+    enabled = in_params.enabled;
+
+    error_info.error_code = ResultSuccess;
+    error_info.address = CpuAddr(0);
+}
+
+void CompressorInfo::UpdateForCommandGeneration() {
+    if (enabled) {
+        usage_state = UsageState::Enabled;
+    } else {
+        usage_state = UsageState::Disabled;
+    }
+
+    auto params{reinterpret_cast<ParameterVersion1*>(parameter.data())};
+    params->state = ParameterState::Updated;
+}
+
+CpuAddr CompressorInfo::GetWorkbuffer(s32 index) {
+    return GetSingleBuffer(index);
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/effect/compressor.h b/src/audio_core/renderer/effect/compressor.h
new file mode 100644
index 0000000000..019a5ae58a
--- /dev/null
+++ b/src/audio_core/renderer/effect/compressor.h
@@ -0,0 +1,106 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+
+#include "audio_core/common/common.h"
+#include "audio_core/renderer/effect/effect_info_base.h"
+#include "common/common_types.h"
+#include "common/fixed_point.h"
+
+namespace AudioCore::AudioRenderer {
+
+class CompressorInfo : public EffectInfoBase {
+public:
+    struct ParameterVersion1 {
+        /* 0x00 */ std::array<s8, MaxChannels> inputs;
+        /* 0x06 */ std::array<s8, MaxChannels> outputs;
+        /* 0x0C */ s16 channel_count_max;
+        /* 0x0E */ s16 channel_count;
+        /* 0x10 */ s32 sample_rate;
+        /* 0x14 */ f32 threshold;
+        /* 0x18 */ f32 compressor_ratio;
+        /* 0x1C */ s32 attack_time;
+        /* 0x20 */ s32 release_time;
+        /* 0x24 */ f32 unk_24;
+        /* 0x28 */ f32 unk_28;
+        /* 0x2C */ f32 unk_2C;
+        /* 0x30 */ f32 out_gain;
+        /* 0x34 */ ParameterState state;
+        /* 0x35 */ bool makeup_gain_enabled;
+    };
+    static_assert(sizeof(ParameterVersion1) <= sizeof(EffectInfoBase::InParameterVersion1),
+                  "CompressorInfo::ParameterVersion1 has the wrong size!");
+
+    struct ParameterVersion2 {
+        /* 0x00 */ std::array<s8, MaxChannels> inputs;
+        /* 0x06 */ std::array<s8, MaxChannels> outputs;
+        /* 0x0C */ s16 channel_count_max;
+        /* 0x0E */ s16 channel_count;
+        /* 0x10 */ s32 sample_rate;
+        /* 0x14 */ f32 threshold;
+        /* 0x18 */ f32 compressor_ratio;
+        /* 0x1C */ s32 attack_time;
+        /* 0x20 */ s32 release_time;
+        /* 0x24 */ f32 unk_24;
+        /* 0x28 */ f32 unk_28;
+        /* 0x2C */ f32 unk_2C;
+        /* 0x30 */ f32 out_gain;
+        /* 0x34 */ ParameterState state;
+        /* 0x35 */ bool makeup_gain_enabled;
+    };
+    static_assert(sizeof(ParameterVersion2) <= sizeof(EffectInfoBase::InParameterVersion2),
+                  "CompressorInfo::ParameterVersion2 has the wrong size!");
+
+    struct State {
+        f32 unk_00;
+        f32 unk_04;
+        f32 unk_08;
+        f32 unk_0C;
+        f32 unk_10;
+        f32 unk_14;
+        f32 unk_18;
+        f32 makeup_gain;
+        f32 unk_20;
+        char unk_24[0x1C];
+    };
+    static_assert(sizeof(State) <= sizeof(EffectInfoBase::State),
+                  "CompressorInfo::State has the wrong size!");
+
+    /**
+     * Update the info with new parameters, version 1.
+     *
+     * @param error_info  - Used to write call result code.
+     * @param in_params   - New parameters to update the info with.
+     * @param pool_mapper - Pool for mapping buffers.
+     */
+    void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params,
+                const PoolMapper& pool_mapper) override;
+
+    /**
+     * Update the info with new parameters, version 2.
+     *
+     * @param error_info  - Used to write call result code.
+     * @param in_params   - New parameters to update the info with.
+     * @param pool_mapper - Pool for mapping buffers.
+     */
+    void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params,
+                const PoolMapper& pool_mapper) override;
+
+    /**
+     * Update the info after command generation. Usually only changes its state.
+     */
+    void UpdateForCommandGeneration() override;
+
+    /**
+     * Get a workbuffer assigned to this effect with the given index.
+     *
+     * @param index - Workbuffer index.
+     * @return Address of the buffer.
+     */
+    CpuAddr GetWorkbuffer(s32 index) override;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/effect/delay.cpp b/src/audio_core/renderer/effect/delay.cpp
new file mode 100644
index 0000000000..d9853efd92
--- /dev/null
+++ b/src/audio_core/renderer/effect/delay.cpp
@@ -0,0 +1,93 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/effect/delay.h"
+
+namespace AudioCore::AudioRenderer {
+
+void DelayInfo::Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params,
+                       const PoolMapper& pool_mapper) {
+    auto in_specific{reinterpret_cast<const ParameterVersion1*>(in_params.specific.data())};
+    auto params{reinterpret_cast<ParameterVersion1*>(parameter.data())};
+
+    if (IsChannelCountValid(in_specific->channel_count_max)) {
+        const auto old_state{params->state};
+        std::memcpy(params, in_specific, sizeof(ParameterVersion1));
+        mix_id = in_params.mix_id;
+        process_order = in_params.process_order;
+        enabled = in_params.enabled;
+
+        if (!IsChannelCountValid(in_specific->channel_count)) {
+            params->channel_count = params->channel_count_max;
+        }
+
+        if (!IsChannelCountValid(in_specific->channel_count) ||
+            old_state != ParameterState::Updated) {
+            params->state = old_state;
+        }
+
+        if (buffer_unmapped || in_params.is_new) {
+            usage_state = UsageState::New;
+            params->state = ParameterState::Initialized;
+            buffer_unmapped = !pool_mapper.TryAttachBuffer(
+                error_info, workbuffers[0], in_params.workbuffer, in_params.workbuffer_size);
+            return;
+        }
+    }
+    error_info.error_code = ResultSuccess;
+    error_info.address = CpuAddr(0);
+}
+
+void DelayInfo::Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params,
+                       const PoolMapper& pool_mapper) {
+    auto in_specific{reinterpret_cast<const ParameterVersion1*>(in_params.specific.data())};
+    auto params{reinterpret_cast<ParameterVersion1*>(parameter.data())};
+
+    if (IsChannelCountValid(in_specific->channel_count_max)) {
+        const auto old_state{params->state};
+        std::memcpy(params, in_specific, sizeof(ParameterVersion1));
+        mix_id = in_params.mix_id;
+        process_order = in_params.process_order;
+        enabled = in_params.enabled;
+
+        if (!IsChannelCountValid(in_specific->channel_count)) {
+            params->channel_count = params->channel_count_max;
+        }
+
+        if (!IsChannelCountValid(in_specific->channel_count) ||
+            old_state != ParameterState::Updated) {
+            params->state = old_state;
+        }
+
+        if (buffer_unmapped || in_params.is_new) {
+            usage_state = UsageState::New;
+            params->state = ParameterState::Initialized;
+            buffer_unmapped = !pool_mapper.TryAttachBuffer(
+                error_info, workbuffers[0], in_params.workbuffer, in_params.workbuffer_size);
+            return;
+        }
+    }
+    error_info.error_code = ResultSuccess;
+    error_info.address = CpuAddr(0);
+}
+
+void DelayInfo::UpdateForCommandGeneration() {
+    if (enabled) {
+        usage_state = UsageState::Enabled;
+    } else {
+        usage_state = UsageState::Disabled;
+    }
+
+    auto params{reinterpret_cast<ParameterVersion1*>(parameter.data())};
+    params->state = ParameterState::Updated;
+}
+
+void DelayInfo::InitializeResultState(EffectResultState& result_state) {}
+
+void DelayInfo::UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) {}
+
+CpuAddr DelayInfo::GetWorkbuffer(s32 index) {
+    return GetSingleBuffer(index);
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/effect/delay.h b/src/audio_core/renderer/effect/delay.h
new file mode 100644
index 0000000000..accc42a060
--- /dev/null
+++ b/src/audio_core/renderer/effect/delay.h
@@ -0,0 +1,135 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+#include <vector>
+
+#include "audio_core/common/common.h"
+#include "audio_core/renderer/effect/effect_info_base.h"
+#include "common/common_types.h"
+#include "common/fixed_point.h"
+
+namespace AudioCore::AudioRenderer {
+
+class DelayInfo : public EffectInfoBase {
+public:
+    struct ParameterVersion1 {
+        /* 0x00 */ std::array<s8, MaxChannels> inputs;
+        /* 0x06 */ std::array<s8, MaxChannels> outputs;
+        /* 0x0C */ u16 channel_count_max;
+        /* 0x0E */ u16 channel_count;
+        /* 0x10 */ u32 delay_time_max;
+        /* 0x14 */ u32 delay_time;
+        /* 0x18 */ Common::FixedPoint<18, 14> sample_rate;
+        /* 0x1C */ Common::FixedPoint<18, 14> in_gain;
+        /* 0x20 */ Common::FixedPoint<18, 14> feedback_gain;
+        /* 0x24 */ Common::FixedPoint<18, 14> wet_gain;
+        /* 0x28 */ Common::FixedPoint<18, 14> dry_gain;
+        /* 0x2C */ Common::FixedPoint<18, 14> channel_spread;
+        /* 0x30 */ Common::FixedPoint<18, 14> lowpass_amount;
+        /* 0x34 */ ParameterState state;
+    };
+    static_assert(sizeof(ParameterVersion1) <= sizeof(EffectInfoBase::InParameterVersion1),
+                  "DelayInfo::ParameterVersion1 has the wrong size!");
+
+    struct ParameterVersion2 {
+        /* 0x00 */ std::array<s8, MaxChannels> inputs;
+        /* 0x06 */ std::array<s8, MaxChannels> outputs;
+        /* 0x0C */ s16 channel_count_max;
+        /* 0x0E */ s16 channel_count;
+        /* 0x10 */ s32 delay_time_max;
+        /* 0x14 */ s32 delay_time;
+        /* 0x18 */ s32 sample_rate;
+        /* 0x1C */ s32 in_gain;
+        /* 0x20 */ s32 feedback_gain;
+        /* 0x24 */ s32 wet_gain;
+        /* 0x28 */ s32 dry_gain;
+        /* 0x2C */ s32 channel_spread;
+        /* 0x30 */ s32 lowpass_amount;
+        /* 0x34 */ ParameterState state;
+    };
+    static_assert(sizeof(ParameterVersion2) <= sizeof(EffectInfoBase::InParameterVersion2),
+                  "DelayInfo::ParameterVersion2 has the wrong size!");
+
+    struct DelayLine {
+        Common::FixedPoint<50, 14> Read() const {
+            return buffer[buffer_pos];
+        }
+
+        void Write(const Common::FixedPoint<50, 14> value) {
+            buffer[buffer_pos] = value;
+            buffer_pos = static_cast<u32>((buffer_pos + 1) % buffer.size());
+        }
+
+        s32 sample_count_max{};
+        s32 sample_count{};
+        std::vector<Common::FixedPoint<50, 14>> buffer{};
+        u32 buffer_pos{};
+        Common::FixedPoint<18, 14> decay_rate{};
+    };
+
+    struct State {
+        /* 0x000 */ std::array<s32, 8> unk_000;
+        /* 0x020 */ std::array<DelayLine, MaxChannels> delay_lines;
+        /* 0x0B0 */ Common::FixedPoint<18, 14> feedback_gain;
+        /* 0x0B4 */ Common::FixedPoint<18, 14> delay_feedback_gain;
+        /* 0x0B8 */ Common::FixedPoint<18, 14> delay_feedback_cross_gain;
+        /* 0x0BC */ Common::FixedPoint<18, 14> lowpass_gain;
+        /* 0x0C0 */ Common::FixedPoint<18, 14> lowpass_feedback_gain;
+        /* 0x0C4 */ std::array<Common::FixedPoint<50, 14>, MaxChannels> lowpass_z;
+    };
+    static_assert(sizeof(State) <= sizeof(EffectInfoBase::State),
+                  "DelayInfo::State has the wrong size!");
+
+    /**
+     * Update the info with new parameters, version 1.
+     *
+     * @param error_info  - Used to write call result code.
+     * @param in_params   - New parameters to update the info with.
+     * @param pool_mapper - Pool for mapping buffers.
+     */
+    void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params,
+                const PoolMapper& pool_mapper) override;
+
+    /**
+     * Update the info with new parameters, version 2.
+     *
+     * @param error_info  - Used to write call result code.
+     * @param in_params   - New parameters to update the info with.
+     * @param pool_mapper - Pool for mapping buffers.
+     */
+    void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params,
+                const PoolMapper& pool_mapper) override;
+
+    /**
+     * Update the info after command generation. Usually only changes its state.
+     */
+    void UpdateForCommandGeneration() override;
+
+    /**
+     * Initialize a new result state. Version 2 only, unused.
+     *
+     * @param result_state - Result state to initialize.
+     */
+    void InitializeResultState(EffectResultState& result_state) override;
+
+    /**
+     * Update the host-side state with the ADSP-side state. Version 2 only, unused.
+     *
+     * @param cpu_state - Host-side result state to update.
+     * @param dsp_state - AudioRenderer-side result state to update from.
+     */
+    void UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) override;
+
+    /**
+     * Get a workbuffer assigned to this effect with the given index.
+     *
+     * @param index - Workbuffer index.
+     * @return Address of the buffer.
+     */
+    CpuAddr GetWorkbuffer(s32 index) override;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/effect/effect_context.cpp b/src/audio_core/renderer/effect/effect_context.cpp
new file mode 100644
index 0000000000..74c7801c9a
--- /dev/null
+++ b/src/audio_core/renderer/effect/effect_context.cpp
@@ -0,0 +1,41 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/effect/effect_context.h"
+
+namespace AudioCore::AudioRenderer {
+
+void EffectContext::Initialize(std::span<EffectInfoBase> effect_infos_, const u32 effect_count_,
+                               std::span<EffectResultState> result_states_cpu_,
+                               std::span<EffectResultState> result_states_dsp_,
+                               const size_t dsp_state_count_) {
+    effect_infos = effect_infos_;
+    effect_count = effect_count_;
+    result_states_cpu = result_states_cpu_;
+    result_states_dsp = result_states_dsp_;
+    dsp_state_count = dsp_state_count_;
+}
+
+EffectInfoBase& EffectContext::GetInfo(const u32 index) {
+    return effect_infos[index];
+}
+
+EffectResultState& EffectContext::GetResultState(const u32 index) {
+    return result_states_cpu[index];
+}
+
+EffectResultState& EffectContext::GetDspSharedResultState(const u32 index) {
+    return result_states_dsp[index];
+}
+
+u32 EffectContext::GetCount() const {
+    return effect_count;
+}
+
+void EffectContext::UpdateStateByDspShared() {
+    for (size_t i = 0; i < dsp_state_count; i++) {
+        effect_infos[i].UpdateResultState(result_states_cpu[i], result_states_dsp[i]);
+    }
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/effect/effect_context.h b/src/audio_core/renderer/effect/effect_context.h
new file mode 100644
index 0000000000..85955bd9c9
--- /dev/null
+++ b/src/audio_core/renderer/effect/effect_context.h
@@ -0,0 +1,75 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <span>
+
+#include "audio_core/renderer/effect/effect_info_base.h"
+#include "audio_core/renderer/effect/effect_result_state.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+
+class EffectContext {
+public:
+    /**
+     * Initialize the effect context
+     * @param effect_infos List of effect infos for this context
+     * @param effect_count The number of effects in the list
+     * @param result_states_cpu The workbuffer of result states for the CPU for this context
+     * @param result_states_dsp The workbuffer of result states for the DSP for this context
+     * @param state_count The number of result states
+     */
+    void Initialize(std::span<EffectInfoBase> effect_infos_, const u32 effect_count_,
+                    std::span<EffectResultState> result_states_cpu_,
+                    std::span<EffectResultState> result_states_dsp_, const size_t dsp_state_count);
+
+    /**
+     * Get the EffectInfo for a given index
+     * @param index Which effect to return
+     * @return Pointer to the effect
+     */
+    EffectInfoBase& GetInfo(const u32 index);
+
+    /**
+     * Get the CPU result state for a given index
+     * @param index Which result to return
+     * @return Pointer to the effect result state
+     */
+    EffectResultState& GetResultState(const u32 index);
+
+    /**
+     * Get the DSP result state for a given index
+     * @param index Which result to return
+     * @return Pointer to the effect result state
+     */
+    EffectResultState& GetDspSharedResultState(const u32 index);
+
+    /**
+     * Get the number of effects in this context
+     * @return The number of effects
+     */
+    u32 GetCount() const;
+
+    /**
+     * Update the CPU and DSP result states for all effects
+     */
+    void UpdateStateByDspShared();
+
+private:
+    /// Workbuffer for all of the effects
+    std::span<EffectInfoBase> effect_infos{};
+    /// Number of effects in the workbuffer
+    u32 effect_count{};
+    /// Workbuffer of states for all effects, kept host-side and not directly modified, dsp states
+    /// are copied here on the next render frame
+    std::span<EffectResultState> result_states_cpu{};
+    /// Workbuffer of states for all effects, used by the AudioRenderer to track effect state
+    /// between calls
+    std::span<EffectResultState> result_states_dsp{};
+    /// Number of result states in the workbuffers
+    size_t dsp_state_count{};
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/effect/effect_info_base.h b/src/audio_core/renderer/effect/effect_info_base.h
new file mode 100644
index 0000000000..43d0589cc9
--- /dev/null
+++ b/src/audio_core/renderer/effect/effect_info_base.h
@@ -0,0 +1,435 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+
+#include "audio_core/common/common.h"
+#include "audio_core/renderer/behavior/behavior_info.h"
+#include "audio_core/renderer/effect/effect_result_state.h"
+#include "audio_core/renderer/memory/address_info.h"
+#include "audio_core/renderer/memory/pool_mapper.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * Base of all effects. Holds various data and functions used for all derived effects.
+ * Should not be used directly.
+ */
+class EffectInfoBase {
+public:
+    enum class Type : u8 {
+        Invalid,
+        Mix,
+        Aux,
+        Delay,
+        Reverb,
+        I3dl2Reverb,
+        BiquadFilter,
+        LightLimiter,
+        Capture,
+        Compressor,
+    };
+
+    enum class UsageState {
+        Invalid,
+        New,
+        Enabled,
+        Disabled,
+    };
+
+    enum class OutStatus : u8 {
+        Invalid,
+        New,
+        Initialized,
+        Used,
+        Removed,
+    };
+
+    enum class ParameterState : u8 {
+        Initialized,
+        Updating,
+        Updated,
+    };
+
+    struct InParameterVersion1 {
+        /* 0x00 */ Type type;
+        /* 0x01 */ bool is_new;
+        /* 0x02 */ bool enabled;
+        /* 0x04 */ u32 mix_id;
+        /* 0x08 */ CpuAddr workbuffer;
+        /* 0x10 */ CpuAddr workbuffer_size;
+        /* 0x18 */ u32 process_order;
+        /* 0x1C */ char unk1C[0x4];
+        /* 0x20 */ std::array<u8, 0xA0> specific;
+    };
+    static_assert(sizeof(InParameterVersion1) == 0xC0,
+                  "EffectInfoBase::InParameterVersion1 has the wrong size!");
+
+    struct InParameterVersion2 {
+        /* 0x00 */ Type type;
+        /* 0x01 */ bool is_new;
+        /* 0x02 */ bool enabled;
+        /* 0x04 */ u32 mix_id;
+        /* 0x08 */ CpuAddr workbuffer;
+        /* 0x10 */ CpuAddr workbuffer_size;
+        /* 0x18 */ u32 process_order;
+        /* 0x1C */ char unk1C[0x4];
+        /* 0x20 */ std::array<u8, 0xA0> specific;
+    };
+    static_assert(sizeof(InParameterVersion2) == 0xC0,
+                  "EffectInfoBase::InParameterVersion2 has the wrong size!");
+
+    struct OutStatusVersion1 {
+        /* 0x00 */ OutStatus state;
+        /* 0x01 */ char unk01[0xF];
+    };
+    static_assert(sizeof(OutStatusVersion1) == 0x10,
+                  "EffectInfoBase::OutStatusVersion1 has the wrong size!");
+
+    struct OutStatusVersion2 {
+        /* 0x00 */ OutStatus state;
+        /* 0x01 */ char unk01[0xF];
+        /* 0x10 */ EffectResultState result_state;
+    };
+    static_assert(sizeof(OutStatusVersion2) == 0x90,
+                  "EffectInfoBase::OutStatusVersion2 has the wrong size!");
+
+    struct State {
+        std::array<u8, 0x500> buffer;
+    };
+    static_assert(sizeof(State) == 0x500, "EffectInfoBase::State has the wrong size!");
+
+    EffectInfoBase() {
+        Cleanup();
+    }
+
+    virtual ~EffectInfoBase() = default;
+
+    /**
+     * Cleanup this effect, resetting it to a starting state.
+     */
+    void Cleanup() {
+        type = Type::Invalid;
+        enabled = false;
+        mix_id = UnusedMixId;
+        process_order = InvalidProcessOrder;
+        buffer_unmapped = false;
+        parameter = {};
+        for (auto& workbuffer : workbuffers) {
+            workbuffer.Setup(CpuAddr(0), 0);
+        }
+    }
+
+    /**
+     * Forcibly unmap all assigned workbuffers from the AudioRenderer.
+     *
+     * @param pool_mapper - Mapper to unmap the buffers.
+     */
+    void ForceUnmapBuffers(const PoolMapper& pool_mapper) {
+        for (auto& workbuffer : workbuffers) {
+            if (workbuffer.GetReference(false) != 0) {
+                pool_mapper.ForceUnmapPointer(workbuffer);
+            }
+        }
+    }
+
+    /**
+     * Check if this effect is enabled.
+     *
+     * @return True if effect is enabled, otherwise false.
+     */
+    bool IsEnabled() const {
+        return enabled;
+    }
+
+    /**
+     * Check if this effect should not be generated.
+     *
+     * @return True if effect should be skipped, otherwise false.
+     */
+    bool ShouldSkip() const {
+        return buffer_unmapped;
+    }
+
+    /**
+     * Get the type of this effect.
+     *
+     * @return The type of this effect. See EffectInfoBase::Type
+     */
+    Type GetType() const {
+        return type;
+    }
+
+    /**
+     * Set the type of this effect.
+     *
+     * @param type_ - The new type of this effect.
+     */
+    void SetType(const Type type_) {
+        type = type_;
+    }
+
+    /**
+     * Get the mix id of this effect.
+     *
+     * @return Mix id of this effect.
+     */
+    s32 GetMixId() const {
+        return mix_id;
+    }
+
+    /**
+     * Get the processing order of this effect.
+     *
+     * @return Process order of this effect.
+     */
+    s32 GetProcessingOrder() const {
+        return process_order;
+    }
+
+    /**
+     * Get this effect's parameter data.
+     *
+     * @return Pointer to the parametter, must be cast to the correct type.
+     */
+    u8* GetParameter() {
+        return parameter.data();
+    }
+
+    /**
+     * Get this effect's parameter data.
+     *
+     * @return Pointer to the parametter, must be cast to the correct type.
+     */
+    u8* GetStateBuffer() {
+        return state.data();
+    }
+
+    /**
+     * Set this effect's usage state.
+     *
+     * @param usage - new usage state of this effect.
+     */
+    void SetUsage(const UsageState usage) {
+        usage_state = usage;
+    }
+
+    /**
+     * Check if this effects need to have its workbuffer information updated.
+     * Version 1.
+     *
+     * @param params - Input parameters.
+     * @return True if workbuffers need updating, otherwise false.
+     */
+    bool ShouldUpdateWorkBufferInfo(const InParameterVersion1& params) const {
+        return buffer_unmapped || params.is_new;
+    }
+
+    /**
+     * Check if this effects need to have its workbuffer information updated.
+     * Version 2.
+     *
+     * @param params - Input parameters.
+     * @return True if workbuffers need updating, otherwise false.
+     */
+    bool ShouldUpdateWorkBufferInfo(const InParameterVersion2& params) const {
+        return buffer_unmapped || params.is_new;
+    }
+
+    /**
+     * Get the current usage state of this effect.
+     *
+     * @return The current usage state.
+     */
+    UsageState GetUsage() const {
+        return usage_state;
+    }
+
+    /**
+     * Write the current state. Version 1.
+     *
+     * @param out_status      - Status to write.
+     * @param renderer_active - Is the AudioRenderer active?
+     */
+    void StoreStatus(OutStatusVersion1& out_status, const bool renderer_active) const {
+        if (renderer_active) {
+            if (usage_state != UsageState::Disabled) {
+                out_status.state = OutStatus::Used;
+            } else {
+                out_status.state = OutStatus::Removed;
+            }
+        } else if (usage_state == UsageState::New) {
+            out_status.state = OutStatus::Used;
+        } else {
+            out_status.state = OutStatus::Removed;
+        }
+    }
+
+    /**
+     * Write the current state. Version 2.
+     *
+     * @param out_status      - Status to write.
+     * @param renderer_active - Is the AudioRenderer active?
+     */
+    void StoreStatus(OutStatusVersion2& out_status, const bool renderer_active) const {
+        if (renderer_active) {
+            if (usage_state != UsageState::Disabled) {
+                out_status.state = OutStatus::Used;
+            } else {
+                out_status.state = OutStatus::Removed;
+            }
+        } else if (usage_state == UsageState::New) {
+            out_status.state = OutStatus::Used;
+        } else {
+            out_status.state = OutStatus::Removed;
+        }
+    }
+
+    /**
+     * Update the info with new parameters, version 1.
+     *
+     * @param error_info  - Used to write call result code.
+     * @param in_params   - New parameters to update the info with.
+     * @param pool_mapper - Pool for mapping buffers.
+     */
+    virtual void Update(BehaviorInfo::ErrorInfo& error_info,
+                        [[maybe_unused]] const InParameterVersion1& params,
+                        [[maybe_unused]] const PoolMapper& pool_mapper) {
+        error_info.error_code = ResultSuccess;
+        error_info.address = CpuAddr(0);
+    }
+
+    /**
+     * Update the info with new parameters, version 2.
+     *
+     * @param error_info  - Used to write call result code.
+     * @param in_params   - New parameters to update the info with.
+     * @param pool_mapper - Pool for mapping buffers.
+     */
+    virtual void Update(BehaviorInfo::ErrorInfo& error_info,
+                        [[maybe_unused]] const InParameterVersion2& params,
+                        [[maybe_unused]] const PoolMapper& pool_mapper) {
+        error_info.error_code = ResultSuccess;
+        error_info.address = CpuAddr(0);
+    }
+
+    /**
+     * Update the info after command generation. Usually only changes its state.
+     */
+    virtual void UpdateForCommandGeneration() {}
+
+    /**
+     * Initialize a new result state. Version 2 only, unused.
+     *
+     * @param result_state - Result state to initialize.
+     */
+    virtual void InitializeResultState([[maybe_unused]] EffectResultState& result_state) {}
+
+    /**
+     * Update the host-side state with the ADSP-side state. Version 2 only, unused.
+     *
+     * @param cpu_state - Host-side result state to update.
+     * @param dsp_state - AudioRenderer-side result state to update from.
+     */
+    virtual void UpdateResultState([[maybe_unused]] EffectResultState& cpu_state,
+                                   [[maybe_unused]] EffectResultState& dsp_state) {}
+
+    /**
+     * Get a workbuffer assigned to this effect with the given index.
+     *
+     * @param index - Workbuffer index.
+     * @return Address of the buffer.
+     */
+    virtual CpuAddr GetWorkbuffer([[maybe_unused]] s32 index) {
+        return 0;
+    }
+
+    /**
+     * Get the first workbuffer assigned to this effect.
+     *
+     * @param index - Workbuffer index. Unused.
+     * @return Address of the buffer.
+     */
+    CpuAddr GetSingleBuffer([[maybe_unused]] const s32 index) {
+        if (enabled) {
+            return workbuffers[0].GetReference(true);
+        }
+
+        if (usage_state != UsageState::Disabled) {
+            const auto ref{workbuffers[0].GetReference(false)};
+            const auto size{workbuffers[0].GetSize()};
+            if (ref != 0 && size > 0) {
+                // Invalidate DSP cache
+            }
+        }
+        return 0;
+    }
+
+    /**
+     * Get the send buffer info, used by Aux and Capture.
+     *
+     * @return Address of the buffer info.
+     */
+    CpuAddr GetSendBufferInfo() const {
+        return send_buffer_info;
+    }
+
+    /**
+     * Get the send buffer, used by Aux and Capture.
+     *
+     * @return Address of the buffer.
+     */
+    CpuAddr GetSendBuffer() const {
+        return send_buffer;
+    }
+
+    /**
+     * Get the return buffer info, used by Aux and Capture.
+     *
+     * @return Address of the buffer info.
+     */
+    CpuAddr GetReturnBufferInfo() const {
+        return return_buffer_info;
+    }
+
+    /**
+     * Get the return buffer, used by Aux and Capture.
+     *
+     * @return Address of the buffer.
+     */
+    CpuAddr GetReturnBuffer() const {
+        return return_buffer;
+    }
+
+protected:
+    /// Type of this effect. May be changed
+    Type type{Type::Invalid};
+    /// Is this effect enabled?
+    bool enabled{};
+    /// Are this effect's buffers unmapped?
+    bool buffer_unmapped{};
+    /// Current usage state
+    UsageState usage_state{UsageState::Invalid};
+    /// Mix id of this effect
+    s32 mix_id{UnusedMixId};
+    /// Process order of this effect
+    s32 process_order{InvalidProcessOrder};
+    /// Workbuffers assigned to this effect
+    std::array<AddressInfo, 2> workbuffers{AddressInfo(CpuAddr(0), 0), AddressInfo(CpuAddr(0), 0)};
+    /// Aux/Capture buffer info for reading
+    CpuAddr send_buffer_info;
+    /// Aux/Capture buffer for reading
+    CpuAddr send_buffer;
+    /// Aux/Capture buffer info for writing
+    CpuAddr return_buffer_info;
+    /// Aux/Capture buffer for writing
+    CpuAddr return_buffer;
+    /// Parameters of this effect
+    std::array<u8, sizeof(InParameterVersion2)> parameter{};
+    /// State of this effect used by the AudioRenderer across calls
+    std::array<u8, sizeof(State)> state{};
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/effect/effect_reset.h b/src/audio_core/renderer/effect/effect_reset.h
new file mode 100644
index 0000000000..1ea67e3347
--- /dev/null
+++ b/src/audio_core/renderer/effect/effect_reset.h
@@ -0,0 +1,71 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#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/compressor.h"
+#include "audio_core/renderer/effect/delay.h"
+#include "audio_core/renderer/effect/i3dl2.h"
+#include "audio_core/renderer/effect/light_limiter.h"
+#include "audio_core/renderer/effect/reverb.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * Reset an effect, and create a new one of the given type.
+ *
+ * @param effect - Effect to reset and re-construct.
+ * @param type   - Type of the new effect to create.
+ */
+static void ResetEffect(EffectInfoBase* effect, const EffectInfoBase::Type type) {
+    *effect = {};
+
+    switch (type) {
+    case EffectInfoBase::Type::Invalid:
+        std::construct_at<EffectInfoBase>(effect);
+        effect->SetType(EffectInfoBase::Type::Invalid);
+        break;
+    case EffectInfoBase::Type::Mix:
+        std::construct_at<BufferMixerInfo>(reinterpret_cast<BufferMixerInfo*>(effect));
+        effect->SetType(EffectInfoBase::Type::Mix);
+        break;
+    case EffectInfoBase::Type::Aux:
+        std::construct_at<AuxInfo>(reinterpret_cast<AuxInfo*>(effect));
+        effect->SetType(EffectInfoBase::Type::Aux);
+        break;
+    case EffectInfoBase::Type::Delay:
+        std::construct_at<DelayInfo>(reinterpret_cast<DelayInfo*>(effect));
+        effect->SetType(EffectInfoBase::Type::Delay);
+        break;
+    case EffectInfoBase::Type::Reverb:
+        std::construct_at<ReverbInfo>(reinterpret_cast<ReverbInfo*>(effect));
+        effect->SetType(EffectInfoBase::Type::Reverb);
+        break;
+    case EffectInfoBase::Type::I3dl2Reverb:
+        std::construct_at<I3dl2ReverbInfo>(reinterpret_cast<I3dl2ReverbInfo*>(effect));
+        effect->SetType(EffectInfoBase::Type::I3dl2Reverb);
+        break;
+    case EffectInfoBase::Type::BiquadFilter:
+        std::construct_at<BiquadFilterInfo>(reinterpret_cast<BiquadFilterInfo*>(effect));
+        effect->SetType(EffectInfoBase::Type::BiquadFilter);
+        break;
+    case EffectInfoBase::Type::LightLimiter:
+        std::construct_at<LightLimiterInfo>(reinterpret_cast<LightLimiterInfo*>(effect));
+        effect->SetType(EffectInfoBase::Type::LightLimiter);
+        break;
+    case EffectInfoBase::Type::Capture:
+        std::construct_at<CaptureInfo>(reinterpret_cast<CaptureInfo*>(effect));
+        effect->SetType(EffectInfoBase::Type::Capture);
+        break;
+    case EffectInfoBase::Type::Compressor:
+        std::construct_at<CompressorInfo>(reinterpret_cast<CompressorInfo*>(effect));
+        effect->SetType(EffectInfoBase::Type::Compressor);
+        break;
+    }
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/effect/effect_result_state.h b/src/audio_core/renderer/effect/effect_result_state.h
new file mode 100644
index 0000000000..ae096ad69f
--- /dev/null
+++ b/src/audio_core/renderer/effect/effect_result_state.h
@@ -0,0 +1,16 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+
+struct EffectResultState {
+    std::array<u8, 0x80> state;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/effect/i3dl2.cpp b/src/audio_core/renderer/effect/i3dl2.cpp
new file mode 100644
index 0000000000..960b29cfc7
--- /dev/null
+++ b/src/audio_core/renderer/effect/i3dl2.cpp
@@ -0,0 +1,94 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/effect/i3dl2.h"
+
+namespace AudioCore::AudioRenderer {
+
+void I3dl2ReverbInfo::Update(BehaviorInfo::ErrorInfo& error_info,
+                             const InParameterVersion1& in_params, const PoolMapper& pool_mapper) {
+    auto in_specific{reinterpret_cast<const ParameterVersion1*>(in_params.specific.data())};
+    auto params{reinterpret_cast<ParameterVersion1*>(parameter.data())};
+
+    if (IsChannelCountValid(in_specific->channel_count_max)) {
+        const auto old_state{params->state};
+        std::memcpy(params, in_specific, sizeof(ParameterVersion1));
+        mix_id = in_params.mix_id;
+        process_order = in_params.process_order;
+        enabled = in_params.enabled;
+
+        if (!IsChannelCountValid(in_specific->channel_count)) {
+            params->channel_count = params->channel_count_max;
+        }
+
+        if (!IsChannelCountValid(in_specific->channel_count) ||
+            old_state != ParameterState::Updated) {
+            params->state = old_state;
+        }
+
+        if (buffer_unmapped || in_params.is_new) {
+            usage_state = UsageState::New;
+            params->state = ParameterState::Initialized;
+            buffer_unmapped = !pool_mapper.TryAttachBuffer(
+                error_info, workbuffers[0], in_params.workbuffer, in_params.workbuffer_size);
+            return;
+        }
+    }
+    error_info.error_code = ResultSuccess;
+    error_info.address = CpuAddr(0);
+}
+
+void I3dl2ReverbInfo::Update(BehaviorInfo::ErrorInfo& error_info,
+                             const InParameterVersion2& in_params, const PoolMapper& pool_mapper) {
+    auto in_specific{reinterpret_cast<const ParameterVersion1*>(in_params.specific.data())};
+    auto params{reinterpret_cast<ParameterVersion1*>(parameter.data())};
+
+    if (IsChannelCountValid(in_specific->channel_count_max)) {
+        const auto old_state{params->state};
+        std::memcpy(params, in_specific, sizeof(ParameterVersion1));
+        mix_id = in_params.mix_id;
+        process_order = in_params.process_order;
+        enabled = in_params.enabled;
+
+        if (!IsChannelCountValid(in_specific->channel_count)) {
+            params->channel_count = params->channel_count_max;
+        }
+
+        if (!IsChannelCountValid(in_specific->channel_count) ||
+            old_state != ParameterState::Updated) {
+            params->state = old_state;
+        }
+
+        if (buffer_unmapped || in_params.is_new) {
+            usage_state = UsageState::New;
+            params->state = ParameterState::Initialized;
+            buffer_unmapped = !pool_mapper.TryAttachBuffer(
+                error_info, workbuffers[0], in_params.workbuffer, in_params.workbuffer_size);
+            return;
+        }
+    }
+    error_info.error_code = ResultSuccess;
+    error_info.address = CpuAddr(0);
+}
+
+void I3dl2ReverbInfo::UpdateForCommandGeneration() {
+    if (enabled) {
+        usage_state = UsageState::Enabled;
+    } else {
+        usage_state = UsageState::Disabled;
+    }
+
+    auto params{reinterpret_cast<ParameterVersion1*>(parameter.data())};
+    params->state = ParameterState::Updated;
+}
+
+void I3dl2ReverbInfo::InitializeResultState(EffectResultState& result_state) {}
+
+void I3dl2ReverbInfo::UpdateResultState(EffectResultState& cpu_state,
+                                        EffectResultState& dsp_state) {}
+
+CpuAddr I3dl2ReverbInfo::GetWorkbuffer(s32 index) {
+    return GetSingleBuffer(index);
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/effect/i3dl2.h b/src/audio_core/renderer/effect/i3dl2.h
new file mode 100644
index 0000000000..7a088a6270
--- /dev/null
+++ b/src/audio_core/renderer/effect/i3dl2.h
@@ -0,0 +1,200 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+#include <vector>
+
+#include "audio_core/common/common.h"
+#include "audio_core/renderer/effect/effect_info_base.h"
+#include "common/common_types.h"
+#include "common/fixed_point.h"
+
+namespace AudioCore::AudioRenderer {
+
+class I3dl2ReverbInfo : public EffectInfoBase {
+public:
+    struct ParameterVersion1 {
+        /* 0x00 */ std::array<s8, MaxChannels> inputs;
+        /* 0x06 */ std::array<s8, MaxChannels> outputs;
+        /* 0x0C */ u16 channel_count_max;
+        /* 0x0E */ u16 channel_count;
+        /* 0x10 */ char unk10[0x4];
+        /* 0x14 */ u32 sample_rate;
+        /* 0x18 */ f32 room_HF_gain;
+        /* 0x1C */ f32 reference_HF;
+        /* 0x20 */ f32 late_reverb_decay_time;
+        /* 0x24 */ f32 late_reverb_HF_decay_ratio;
+        /* 0x28 */ f32 room_gain;
+        /* 0x2C */ f32 reflection_gain;
+        /* 0x30 */ f32 reverb_gain;
+        /* 0x34 */ f32 late_reverb_diffusion;
+        /* 0x38 */ f32 reflection_delay;
+        /* 0x3C */ f32 late_reverb_delay_time;
+        /* 0x40 */ f32 late_reverb_density;
+        /* 0x44 */ f32 dry_gain;
+        /* 0x48 */ ParameterState state;
+        /* 0x49 */ char unk49[0x3];
+    };
+    static_assert(sizeof(ParameterVersion1) <= sizeof(EffectInfoBase::InParameterVersion1),
+                  "I3dl2ReverbInfo::ParameterVersion1 has the wrong size!");
+
+    struct ParameterVersion2 {
+        /* 0x00 */ std::array<s8, MaxChannels> inputs;
+        /* 0x06 */ std::array<s8, MaxChannels> outputs;
+        /* 0x0C */ u16 channel_count_max;
+        /* 0x0E */ u16 channel_count;
+        /* 0x10 */ char unk10[0x4];
+        /* 0x14 */ u32 sample_rate;
+        /* 0x18 */ f32 room_HF_gain;
+        /* 0x1C */ f32 reference_HF;
+        /* 0x20 */ f32 late_reverb_decay_time;
+        /* 0x24 */ f32 late_reverb_HF_decay_ratio;
+        /* 0x28 */ f32 room_gain;
+        /* 0x2C */ f32 reflection_gain;
+        /* 0x30 */ f32 reverb_gain;
+        /* 0x34 */ f32 late_reverb_diffusion;
+        /* 0x38 */ f32 reflection_delay;
+        /* 0x3C */ f32 late_reverb_delay_time;
+        /* 0x40 */ f32 late_reverb_density;
+        /* 0x44 */ f32 dry_gain;
+        /* 0x48 */ ParameterState state;
+        /* 0x49 */ char unk49[0x3];
+    };
+    static_assert(sizeof(ParameterVersion2) <= sizeof(EffectInfoBase::InParameterVersion2),
+                  "I3dl2ReverbInfo::ParameterVersion2 has the wrong size!");
+
+    static constexpr u32 MaxDelayLines = 4;
+    static constexpr u32 MaxDelayTaps = 20;
+
+    struct I3dl2DelayLine {
+        void Initialize(const s32 delay_time) {
+            max_delay = delay_time;
+            buffer.resize(delay_time + 1, 0);
+            buffer_end = &buffer[delay_time];
+            output = &buffer[0];
+            SetDelay(delay_time);
+            wet_gain = 0.0f;
+        }
+
+        void SetDelay(const s32 delay_time) {
+            if (max_delay < delay_time) {
+                return;
+            }
+            delay = delay_time;
+            input = &buffer[(output - buffer.data() + delay) % (max_delay + 1)];
+        }
+
+        Common::FixedPoint<50, 14> Tick(const Common::FixedPoint<50, 14> sample) {
+            Write(sample);
+
+            auto out_sample{Read()};
+
+            output++;
+            if (output >= buffer_end) {
+                output = buffer.data();
+            }
+
+            return out_sample;
+        }
+
+        Common::FixedPoint<50, 14> Read() {
+            return *output;
+        }
+
+        void Write(const Common::FixedPoint<50, 14> sample) {
+            *(input++) = sample;
+            if (input >= buffer_end) {
+                input = buffer.data();
+            }
+        }
+
+        Common::FixedPoint<50, 14> TapOut(const s32 index) {
+            auto out{input - (index + 1)};
+            if (out < buffer.data()) {
+                out += max_delay + 1;
+            }
+            return *out;
+        }
+
+        std::vector<Common::FixedPoint<50, 14>> buffer{};
+        Common::FixedPoint<50, 14>* buffer_end{};
+        s32 max_delay{};
+        Common::FixedPoint<50, 14>* input{};
+        Common::FixedPoint<50, 14>* output{};
+        s32 delay{};
+        f32 wet_gain{};
+    };
+
+    struct State {
+        f32 lowpass_0;
+        f32 lowpass_1;
+        f32 lowpass_2;
+        I3dl2DelayLine early_delay_line;
+        std::array<s32, MaxDelayTaps> early_tap_steps;
+        f32 early_gain;
+        f32 late_gain;
+        s32 early_to_late_taps;
+        std::array<I3dl2DelayLine, MaxDelayLines> fdn_delay_lines;
+        std::array<I3dl2DelayLine, MaxDelayLines> decay_delay_lines0;
+        std::array<I3dl2DelayLine, MaxDelayLines> decay_delay_lines1;
+        f32 last_reverb_echo;
+        I3dl2DelayLine center_delay_line;
+        std::array<std::array<f32, 3>, MaxDelayLines> lowpass_coeff;
+        std::array<f32, MaxDelayLines> shelf_filter;
+        f32 dry_gain;
+    };
+    static_assert(sizeof(State) <= sizeof(EffectInfoBase::State),
+                  "I3dl2ReverbInfo::State is too large!");
+
+    /**
+     * Update the info with new parameters, version 1.
+     *
+     * @param error_info  - Used to write call result code.
+     * @param in_params   - New parameters to update the info with.
+     * @param pool_mapper - Pool for mapping buffers.
+     */
+    void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params,
+                const PoolMapper& pool_mapper) override;
+
+    /**
+     * Update the info with new parameters, version 2.
+     *
+     * @param error_info  - Used to write call result code.
+     * @param in_params   - New parameters to update the info with.
+     * @param pool_mapper - Pool for mapping buffers.
+     */
+    void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params,
+                const PoolMapper& pool_mapper) override;
+
+    /**
+     * Update the info after command generation. Usually only changes its state.
+     */
+    void UpdateForCommandGeneration() override;
+
+    /**
+     * Initialize a new result state. Version 2 only, unused.
+     *
+     * @param result_state - Result state to initialize.
+     */
+    void InitializeResultState(EffectResultState& result_state) override;
+
+    /**
+     * Update the host-side state with the ADSP-side state. Version 2 only, unused.
+     *
+     * @param cpu_state - Host-side result state to update.
+     * @param dsp_state - AudioRenderer-side result state to update from.
+     */
+    void UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) override;
+
+    /**
+     * Get a workbuffer assigned to this effect with the given index.
+     *
+     * @param index - Workbuffer index.
+     * @return Address of the buffer.
+     */
+    CpuAddr GetWorkbuffer(s32 index) override;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/effect/light_limiter.cpp b/src/audio_core/renderer/effect/light_limiter.cpp
new file mode 100644
index 0000000000..1635a952da
--- /dev/null
+++ b/src/audio_core/renderer/effect/light_limiter.cpp
@@ -0,0 +1,81 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/effect/light_limiter.h"
+
+namespace AudioCore::AudioRenderer {
+
+void LightLimiterInfo::Update(BehaviorInfo::ErrorInfo& error_info,
+                              const InParameterVersion1& in_params, const PoolMapper& pool_mapper) {
+    auto in_specific{reinterpret_cast<const ParameterVersion1*>(in_params.specific.data())};
+    auto params{reinterpret_cast<ParameterVersion1*>(parameter.data())};
+
+    std::memcpy(params, in_specific, sizeof(ParameterVersion1));
+    mix_id = in_params.mix_id;
+    process_order = in_params.process_order;
+    enabled = in_params.enabled;
+
+    if (buffer_unmapped || in_params.is_new) {
+        usage_state = UsageState::New;
+        params->state = ParameterState::Initialized;
+        buffer_unmapped = !pool_mapper.TryAttachBuffer(
+            error_info, workbuffers[0], in_params.workbuffer, in_params.workbuffer_size);
+    } else {
+        error_info.error_code = ResultSuccess;
+        error_info.address = CpuAddr(0);
+    }
+}
+
+void LightLimiterInfo::Update(BehaviorInfo::ErrorInfo& error_info,
+                              const InParameterVersion2& in_params, const PoolMapper& pool_mapper) {
+    auto in_specific{reinterpret_cast<const ParameterVersion1*>(in_params.specific.data())};
+    auto params{reinterpret_cast<ParameterVersion1*>(parameter.data())};
+
+    std::memcpy(params, in_specific, sizeof(ParameterVersion1));
+    mix_id = in_params.mix_id;
+    process_order = in_params.process_order;
+    enabled = in_params.enabled;
+
+    if (buffer_unmapped || in_params.is_new) {
+        usage_state = UsageState::New;
+        params->state = ParameterState::Initialized;
+        buffer_unmapped = !pool_mapper.TryAttachBuffer(
+            error_info, workbuffers[0], in_params.workbuffer, in_params.workbuffer_size);
+    } else {
+        error_info.error_code = ResultSuccess;
+        error_info.address = CpuAddr(0);
+    }
+}
+
+void LightLimiterInfo::UpdateForCommandGeneration() {
+    if (enabled) {
+        usage_state = UsageState::Enabled;
+    } else {
+        usage_state = UsageState::Disabled;
+    }
+
+    auto params{reinterpret_cast<ParameterVersion1*>(parameter.data())};
+    params->state = ParameterState::Updated;
+    params->statistics_reset_required = false;
+}
+
+void LightLimiterInfo::InitializeResultState(EffectResultState& result_state) {
+    auto result_state_{reinterpret_cast<StatisticsInternal*>(result_state.state.data())};
+
+    result_state_->channel_max_sample.fill(0);
+    result_state_->channel_compression_gain_min.fill(1.0f);
+}
+
+void LightLimiterInfo::UpdateResultState(EffectResultState& cpu_state,
+                                         EffectResultState& dsp_state) {
+    auto cpu_statistics{reinterpret_cast<StatisticsInternal*>(cpu_state.state.data())};
+    auto dsp_statistics{reinterpret_cast<StatisticsInternal*>(dsp_state.state.data())};
+
+    *cpu_statistics = *dsp_statistics;
+}
+
+CpuAddr LightLimiterInfo::GetWorkbuffer(s32 index) {
+    return GetSingleBuffer(index);
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/effect/light_limiter.h b/src/audio_core/renderer/effect/light_limiter.h
new file mode 100644
index 0000000000..338d67bbc5
--- /dev/null
+++ b/src/audio_core/renderer/effect/light_limiter.h
@@ -0,0 +1,138 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+#include <vector>
+
+#include "audio_core/common/common.h"
+#include "audio_core/renderer/effect/effect_info_base.h"
+#include "common/common_types.h"
+#include "common/fixed_point.h"
+
+namespace AudioCore::AudioRenderer {
+
+class LightLimiterInfo : public EffectInfoBase {
+public:
+    enum class ProcessingMode {
+        Mode0,
+        Mode1,
+    };
+
+    struct ParameterVersion1 {
+        /* 0x00 */ std::array<s8, MaxChannels> inputs;
+        /* 0x06 */ std::array<s8, MaxChannels> outputs;
+        /* 0x0C */ u16 channel_count_max;
+        /* 0x0E */ u16 channel_count;
+        /* 0x0C */ u32 sample_rate;
+        /* 0x14 */ s32 look_ahead_time_max;
+        /* 0x18 */ s32 attack_time;
+        /* 0x1C */ s32 release_time;
+        /* 0x20 */ s32 look_ahead_time;
+        /* 0x24 */ f32 attack_coeff;
+        /* 0x28 */ f32 release_coeff;
+        /* 0x2C */ f32 threshold;
+        /* 0x30 */ f32 input_gain;
+        /* 0x34 */ f32 output_gain;
+        /* 0x38 */ s32 look_ahead_samples_min;
+        /* 0x3C */ s32 look_ahead_samples_max;
+        /* 0x40 */ ParameterState state;
+        /* 0x41 */ bool statistics_enabled;
+        /* 0x42 */ bool statistics_reset_required;
+        /* 0x43 */ ProcessingMode processing_mode;
+    };
+    static_assert(sizeof(ParameterVersion1) <= sizeof(EffectInfoBase::InParameterVersion1),
+                  "LightLimiterInfo::ParameterVersion1 has the wrong size!");
+
+    struct ParameterVersion2 {
+        /* 0x00 */ std::array<s8, MaxChannels> inputs;
+        /* 0x06 */ std::array<s8, MaxChannels> outputs;
+        /* 0x0C */ u16 channel_count_max;
+        /* 0x0E */ u16 channel_count;
+        /* 0x0C */ u32 sample_rate;
+        /* 0x14 */ s32 look_ahead_time_max;
+        /* 0x18 */ s32 attack_time;
+        /* 0x1C */ s32 release_time;
+        /* 0x20 */ s32 look_ahead_time;
+        /* 0x24 */ f32 attack_coeff;
+        /* 0x28 */ f32 release_coeff;
+        /* 0x2C */ f32 threshold;
+        /* 0x30 */ f32 input_gain;
+        /* 0x34 */ f32 output_gain;
+        /* 0x38 */ s32 look_ahead_samples_min;
+        /* 0x3C */ s32 look_ahead_samples_max;
+        /* 0x40 */ ParameterState state;
+        /* 0x41 */ bool statistics_enabled;
+        /* 0x42 */ bool statistics_reset_required;
+        /* 0x43 */ ProcessingMode processing_mode;
+    };
+    static_assert(sizeof(ParameterVersion2) <= sizeof(EffectInfoBase::InParameterVersion2),
+                  "LightLimiterInfo::ParameterVersion2 has the wrong size!");
+
+    struct State {
+        std::array<Common::FixedPoint<49, 15>, MaxChannels> samples_average;
+        std::array<Common::FixedPoint<49, 15>, MaxChannels> compression_gain;
+        std::array<s32, MaxChannels> look_ahead_sample_offsets;
+        std::array<std::vector<Common::FixedPoint<49, 15>>, MaxChannels> look_ahead_sample_buffers;
+    };
+    static_assert(sizeof(State) <= sizeof(EffectInfoBase::State),
+                  "LightLimiterInfo::State has the wrong size!");
+
+    struct StatisticsInternal {
+        /* 0x00 */ std::array<f32, MaxChannels> channel_max_sample;
+        /* 0x18 */ std::array<f32, MaxChannels> channel_compression_gain_min;
+    };
+    static_assert(sizeof(StatisticsInternal) == 0x30,
+                  "LightLimiterInfo::StatisticsInternal has the wrong size!");
+
+    /**
+     * Update the info with new parameters, version 1.
+     *
+     * @param error_info  - Used to write call result code.
+     * @param in_params   - New parameters to update the info with.
+     * @param pool_mapper - Pool for mapping buffers.
+     */
+    void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params,
+                const PoolMapper& pool_mapper) override;
+
+    /**
+     * Update the info with new parameters, version 2.
+     *
+     * @param error_info  - Used to write call result code.
+     * @param in_params   - New parameters to update the info with.
+     * @param pool_mapper - Pool for mapping buffers.
+     */
+    void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params,
+                const PoolMapper& pool_mapper) override;
+
+    /**
+     * Update the info after command generation. Usually only changes its state.
+     */
+    void UpdateForCommandGeneration() override;
+
+    /**
+     * Initialize a new limiter statistics result state. Version 2 only.
+     *
+     * @param result_state - Result state to initialize.
+     */
+    void InitializeResultState(EffectResultState& result_state) override;
+
+    /**
+     * Update the host-side limiter statistics with the ADSP-side one. Version 2 only.
+     *
+     * @param cpu_state - Host-side result state to update.
+     * @param dsp_state - AudioRenderer-side result state to update from.
+     */
+    void UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) override;
+
+    /**
+     * Get a workbuffer assigned to this effect with the given index.
+     *
+     * @param index - Workbuffer index.
+     * @return Address of the buffer.
+     */
+    CpuAddr GetWorkbuffer(s32 index) override;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/effect/reverb.cpp b/src/audio_core/renderer/effect/reverb.cpp
new file mode 100644
index 0000000000..2d32383d02
--- /dev/null
+++ b/src/audio_core/renderer/effect/reverb.cpp
@@ -0,0 +1,93 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/effect/reverb.h"
+
+namespace AudioCore::AudioRenderer {
+
+void ReverbInfo::Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params,
+                        const PoolMapper& pool_mapper) {
+    auto in_specific{reinterpret_cast<const ParameterVersion1*>(in_params.specific.data())};
+    auto params{reinterpret_cast<ParameterVersion1*>(parameter.data())};
+
+    if (IsChannelCountValid(in_specific->channel_count_max)) {
+        const auto old_state{params->state};
+        std::memcpy(params, in_specific, sizeof(ParameterVersion1));
+        mix_id = in_params.mix_id;
+        process_order = in_params.process_order;
+        enabled = in_params.enabled;
+
+        if (!IsChannelCountValid(in_specific->channel_count)) {
+            params->channel_count = params->channel_count_max;
+        }
+
+        if (!IsChannelCountValid(in_specific->channel_count) ||
+            old_state != ParameterState::Updated) {
+            params->state = old_state;
+        }
+
+        if (buffer_unmapped || in_params.is_new) {
+            usage_state = UsageState::New;
+            params->state = ParameterState::Initialized;
+            buffer_unmapped = !pool_mapper.TryAttachBuffer(
+                error_info, workbuffers[0], in_params.workbuffer, in_params.workbuffer_size);
+            return;
+        }
+    }
+    error_info.error_code = ResultSuccess;
+    error_info.address = CpuAddr(0);
+}
+
+void ReverbInfo::Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params,
+                        const PoolMapper& pool_mapper) {
+    auto in_specific{reinterpret_cast<const ParameterVersion2*>(in_params.specific.data())};
+    auto params{reinterpret_cast<ParameterVersion2*>(parameter.data())};
+
+    if (IsChannelCountValid(in_specific->channel_count_max)) {
+        const auto old_state{params->state};
+        std::memcpy(params, in_specific, sizeof(ParameterVersion2));
+        mix_id = in_params.mix_id;
+        process_order = in_params.process_order;
+        enabled = in_params.enabled;
+
+        if (!IsChannelCountValid(in_specific->channel_count)) {
+            params->channel_count = params->channel_count_max;
+        }
+
+        if (!IsChannelCountValid(in_specific->channel_count) ||
+            old_state != ParameterState::Updated) {
+            params->state = old_state;
+        }
+
+        if (buffer_unmapped || in_params.is_new) {
+            usage_state = UsageState::New;
+            params->state = ParameterState::Initialized;
+            buffer_unmapped = !pool_mapper.TryAttachBuffer(
+                error_info, workbuffers[0], in_params.workbuffer, in_params.workbuffer_size);
+            return;
+        }
+    }
+    error_info.error_code = ResultSuccess;
+    error_info.address = CpuAddr(0);
+}
+
+void ReverbInfo::UpdateForCommandGeneration() {
+    if (enabled) {
+        usage_state = UsageState::Enabled;
+    } else {
+        usage_state = UsageState::Disabled;
+    }
+
+    auto params{reinterpret_cast<ParameterVersion1*>(parameter.data())};
+    params->state = ParameterState::Updated;
+}
+
+void ReverbInfo::InitializeResultState(EffectResultState& result_state) {}
+
+void ReverbInfo::UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) {}
+
+CpuAddr ReverbInfo::GetWorkbuffer(s32 index) {
+    return GetSingleBuffer(index);
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/effect/reverb.h b/src/audio_core/renderer/effect/reverb.h
new file mode 100644
index 0000000000..b4df9f6eff
--- /dev/null
+++ b/src/audio_core/renderer/effect/reverb.h
@@ -0,0 +1,190 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+#include <vector>
+
+#include "audio_core/common/common.h"
+#include "audio_core/renderer/effect/effect_info_base.h"
+#include "common/common_types.h"
+#include "common/fixed_point.h"
+
+namespace AudioCore::AudioRenderer {
+
+class ReverbInfo : public EffectInfoBase {
+public:
+    struct ParameterVersion1 {
+        /* 0x00 */ std::array<s8, MaxChannels> inputs;
+        /* 0x06 */ std::array<s8, MaxChannels> outputs;
+        /* 0x0C */ u16 channel_count_max;
+        /* 0x0E */ u16 channel_count;
+        /* 0x10 */ u32 sample_rate;
+        /* 0x14 */ u32 early_mode;
+        /* 0x18 */ s32 early_gain;
+        /* 0x1C */ s32 pre_delay;
+        /* 0x20 */ s32 late_mode;
+        /* 0x24 */ s32 late_gain;
+        /* 0x28 */ s32 decay_time;
+        /* 0x2C */ s32 high_freq_Decay_ratio;
+        /* 0x30 */ s32 colouration;
+        /* 0x34 */ s32 base_gain;
+        /* 0x38 */ s32 wet_gain;
+        /* 0x3C */ s32 dry_gain;
+        /* 0x40 */ ParameterState state;
+    };
+    static_assert(sizeof(ParameterVersion1) <= sizeof(EffectInfoBase::InParameterVersion1),
+                  "ReverbInfo::ParameterVersion1 has the wrong size!");
+
+    struct ParameterVersion2 {
+        /* 0x00 */ std::array<s8, MaxChannels> inputs;
+        /* 0x06 */ std::array<s8, MaxChannels> outputs;
+        /* 0x0C */ u16 channel_count_max;
+        /* 0x0E */ u16 channel_count;
+        /* 0x10 */ u32 sample_rate;
+        /* 0x14 */ u32 early_mode;
+        /* 0x18 */ s32 early_gain;
+        /* 0x1C */ s32 pre_delay;
+        /* 0x20 */ s32 late_mode;
+        /* 0x24 */ s32 late_gain;
+        /* 0x28 */ s32 decay_time;
+        /* 0x2C */ s32 high_freq_decay_ratio;
+        /* 0x30 */ s32 colouration;
+        /* 0x34 */ s32 base_gain;
+        /* 0x38 */ s32 wet_gain;
+        /* 0x3C */ s32 dry_gain;
+        /* 0x40 */ ParameterState state;
+    };
+    static_assert(sizeof(ParameterVersion2) <= sizeof(EffectInfoBase::InParameterVersion2),
+                  "ReverbInfo::ParameterVersion2 has the wrong size!");
+
+    static constexpr u32 MaxDelayLines = 4;
+    static constexpr u32 MaxDelayTaps = 10;
+    static constexpr u32 NumEarlyModes = 5;
+    static constexpr u32 NumLateModes = 5;
+
+    struct ReverbDelayLine {
+        void Initialize(const s32 delay_time, const f32 decay_rate) {
+            buffer.resize(delay_time + 1, 0);
+            buffer_end = &buffer[delay_time];
+            output = &buffer[0];
+            decay = decay_rate;
+            sample_count_max = delay_time;
+            SetDelay(delay_time);
+        }
+
+        void SetDelay(const s32 delay_time) {
+            if (sample_count_max < delay_time) {
+                return;
+            }
+            sample_count = delay_time;
+            input = &buffer[(output - buffer.data() + sample_count) % (sample_count_max + 1)];
+        }
+
+        Common::FixedPoint<50, 14> Tick(const Common::FixedPoint<50, 14> sample) {
+            Write(sample);
+
+            auto out_sample{Read()};
+
+            output++;
+            if (output >= buffer_end) {
+                output = buffer.data();
+            }
+
+            return out_sample;
+        }
+
+        Common::FixedPoint<50, 14> Read() {
+            return *output;
+        }
+
+        void Write(const Common::FixedPoint<50, 14> sample) {
+            *(input++) = sample;
+            if (input >= buffer_end) {
+                input = buffer.data();
+            }
+        }
+
+        Common::FixedPoint<50, 14> TapOut(const s32 index) {
+            auto out{input - (index + 1)};
+            if (out < buffer.data()) {
+                out += sample_count;
+            }
+            return *out;
+        }
+
+        s32 sample_count{};
+        s32 sample_count_max{};
+        std::vector<Common::FixedPoint<50, 14>> buffer{};
+        Common::FixedPoint<50, 14>* buffer_end;
+        Common::FixedPoint<50, 14>* input{};
+        Common::FixedPoint<50, 14>* output{};
+        Common::FixedPoint<50, 14> decay{};
+    };
+
+    struct State {
+        ReverbDelayLine pre_delay_line;
+        ReverbDelayLine center_delay_line;
+        std::array<s32, MaxDelayTaps> early_delay_times;
+        std::array<Common::FixedPoint<50, 14>, MaxDelayTaps> early_gains;
+        s32 pre_delay_time;
+        std::array<ReverbDelayLine, MaxDelayLines> decay_delay_lines;
+        std::array<ReverbDelayLine, MaxDelayLines> fdn_delay_lines;
+        std::array<Common::FixedPoint<50, 14>, MaxDelayLines> hf_decay_gain;
+        std::array<Common::FixedPoint<50, 14>, MaxDelayLines> hf_decay_prev_gain;
+        std::array<Common::FixedPoint<50, 14>, MaxDelayLines> prev_feedback_output;
+    };
+    static_assert(sizeof(State) <= sizeof(EffectInfoBase::State),
+                  "ReverbInfo::State is too large!");
+
+    /**
+     * Update the info with new parameters, version 1.
+     *
+     * @param error_info  - Used to write call result code.
+     * @param in_params   - New parameters to update the info with.
+     * @param pool_mapper - Pool for mapping buffers.
+     */
+    void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion1& in_params,
+                const PoolMapper& pool_mapper) override;
+
+    /**
+     * Update the info with new parameters, version 2.
+     *
+     * @param error_info  - Used to write call result code.
+     * @param in_params   - New parameters to update the info with.
+     * @param pool_mapper - Pool for mapping buffers.
+     */
+    void Update(BehaviorInfo::ErrorInfo& error_info, const InParameterVersion2& in_params,
+                const PoolMapper& pool_mapper) override;
+
+    /**
+     * Update the info after command generation. Usually only changes its state.
+     */
+    void UpdateForCommandGeneration() override;
+
+    /**
+     * Initialize a new result state. Version 2 only, unused.
+     *
+     * @param result_state - Result state to initialize.
+     */
+    void InitializeResultState(EffectResultState& result_state) override;
+
+    /**
+     * Update the host-side state with the ADSP-side state. Version 2 only, unused.
+     *
+     * @param cpu_state - Host-side result state to update.
+     * @param dsp_state - AudioRenderer-side result state to update from.
+     */
+    void UpdateResultState(EffectResultState& cpu_state, EffectResultState& dsp_state) override;
+
+    /**
+     * Get a workbuffer assigned to this effect with the given index.
+     *
+     * @param index - Workbuffer index.
+     * @return Address of the buffer.
+     */
+    CpuAddr GetWorkbuffer(s32 index) override;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/memory/address_info.h b/src/audio_core/renderer/memory/address_info.h
new file mode 100644
index 0000000000..4cfefea8ed
--- /dev/null
+++ b/src/audio_core/renderer/memory/address_info.h
@@ -0,0 +1,125 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "audio_core/renderer/memory/memory_pool_info.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+
+/**
+ * Represents a region of mapped or unmapped memory.
+ */
+class AddressInfo {
+public:
+    AddressInfo() = default;
+    AddressInfo(CpuAddr cpu_address_, u64 size_) : cpu_address{cpu_address_}, size{size_} {}
+
+    /**
+     * Setup a new AddressInfo.
+     *
+     * @param cpu_address - The CPU address of this region.
+     * @param size        - The size of this region.
+     */
+    void Setup(CpuAddr cpu_address_, u64 size_) {
+        cpu_address = cpu_address_;
+        size = size_;
+        memory_pool = nullptr;
+        dsp_address = 0;
+    }
+
+    /**
+     * Get the CPU address.
+     *
+     * @return The CpuAddr address
+     */
+    CpuAddr GetCpuAddr() const {
+        return cpu_address;
+    }
+
+    /**
+     * Assign this region to a memory pool.
+     *
+     * @param memory_pool_ - Memory pool to assign.
+     * @return The CpuAddr address of this region.
+     */
+    void SetPool(MemoryPoolInfo* memory_pool_) {
+        memory_pool = memory_pool_;
+    }
+
+    /**
+     * Get the size of this region.
+     *
+     * @return The size of this region.
+     */
+    u64 GetSize() const {
+        return size;
+    }
+
+    /**
+     * Get the ADSP address for this region.
+     *
+     * @return The ADSP address for this region.
+     */
+    CpuAddr GetForceMappedDspAddr() const {
+        return dsp_address;
+    }
+
+    /**
+     * Set the ADSP address for this region.
+     *
+     * @param dsp_addr - The new ADSP address for this region.
+     */
+    void SetForceMappedDspAddr(CpuAddr dsp_addr) {
+        dsp_address = dsp_addr;
+    }
+
+    /**
+     * Check whether this region has an active memory pool.
+     *
+     * @return True if this region has a mapped memory pool, otherwise false.
+     */
+    bool HasMappedMemoryPool() const {
+        return memory_pool != nullptr && memory_pool->GetDspAddress() != 0;
+    }
+
+    /**
+     * Check whether this region is mapped to the ADSP.
+     *
+     * @return True if this region is mapped, otherwise false.
+     */
+    bool IsMapped() const {
+        return HasMappedMemoryPool() || dsp_address != 0;
+    }
+
+    /**
+     * Get a usable reference to this region of memory.
+     *
+     * @param mark_in_use - Whether this region should be marked as being in use.
+     * @return A valid memory address if valid, otherwise 0.
+     */
+    CpuAddr GetReference(bool mark_in_use) {
+        if (!HasMappedMemoryPool()) {
+            return dsp_address;
+        }
+
+        if (mark_in_use) {
+            memory_pool->SetUsed(true);
+        }
+
+        return memory_pool->Translate(cpu_address, size);
+    }
+
+private:
+    /// CPU address of this region
+    CpuAddr cpu_address;
+    /// Size of this region
+    u64 size;
+    /// The memory this region is mapped to
+    MemoryPoolInfo* memory_pool;
+    /// ADSP address of this region
+    CpuAddr dsp_address;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/memory/memory_pool_info.cpp b/src/audio_core/renderer/memory/memory_pool_info.cpp
new file mode 100644
index 0000000000..9b7824af1b
--- /dev/null
+++ b/src/audio_core/renderer/memory/memory_pool_info.cpp
@@ -0,0 +1,61 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/memory/memory_pool_info.h"
+
+namespace AudioCore::AudioRenderer {
+
+CpuAddr MemoryPoolInfo::GetCpuAddress() const {
+    return cpu_address;
+}
+
+CpuAddr MemoryPoolInfo::GetDspAddress() const {
+    return dsp_address;
+}
+
+u64 MemoryPoolInfo::GetSize() const {
+    return size;
+}
+
+MemoryPoolInfo::Location MemoryPoolInfo::GetLocation() const {
+    return location;
+}
+
+void MemoryPoolInfo::SetCpuAddress(const CpuAddr address, const u64 size_) {
+    cpu_address = address;
+    size = size_;
+}
+
+void MemoryPoolInfo::SetDspAddress(const CpuAddr address) {
+    dsp_address = address;
+}
+
+bool MemoryPoolInfo::Contains(const CpuAddr address_, const u64 size_) const {
+    return cpu_address <= address_ && (address_ + size_) <= (cpu_address + size);
+}
+
+bool MemoryPoolInfo::IsMapped() const {
+    return dsp_address != 0;
+}
+
+CpuAddr MemoryPoolInfo::Translate(const CpuAddr address, const u64 size_) const {
+    if (!Contains(address, size_)) {
+        return 0;
+    }
+
+    if (!IsMapped()) {
+        return 0;
+    }
+
+    return dsp_address + (address - cpu_address);
+}
+
+void MemoryPoolInfo::SetUsed(const bool used) {
+    in_use = used;
+}
+
+bool MemoryPoolInfo::IsUsed() const {
+    return in_use;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/memory/memory_pool_info.h b/src/audio_core/renderer/memory/memory_pool_info.h
new file mode 100644
index 0000000000..537a466ec7
--- /dev/null
+++ b/src/audio_core/renderer/memory/memory_pool_info.h
@@ -0,0 +1,170 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <memory>
+
+#include "audio_core/common/common.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * CPU pools are mapped in user memory with the supplied process_handle (see PoolMapper).
+ */
+class MemoryPoolInfo {
+public:
+    /**
+     * The location of this pool.
+     * CPU pools are mapped in user memory with the supplied process_handle (see PoolMapper).
+     * DSP pools are mapped in the current process sysmodule.
+     */
+    enum class Location {
+        CPU = 1,
+        DSP = 2,
+    };
+
+    /**
+     * Current state of the pool
+     */
+    enum class State {
+        Invalid,
+        Aquired,
+        RequestDetach,
+        Detached,
+        RequestAttach,
+        Attached,
+        Released,
+    };
+
+    /**
+     * Result code for updating the pool (See InfoUpdater::Update)
+     */
+    enum class ResultState {
+        Success,
+        BadParam,
+        MapFailed,
+        InUse,
+    };
+
+    /**
+     * Input parameters coming from the game which are used to update current pools
+     * (See InfoUpdater::Update)
+     */
+    struct InParameter {
+        /* 0x00 */ u64 address;
+        /* 0x08 */ u64 size;
+        /* 0x10 */ State state;
+        /* 0x14 */ bool in_use;
+        /* 0x18 */ char unk18[0x8];
+    };
+    static_assert(sizeof(InParameter) == 0x20, "MemoryPoolInfo::InParameter has the wrong size!");
+
+    /**
+     * Output status sent back to the game on update (See InfoUpdater::Update)
+     */
+    struct OutStatus {
+        /* 0x00 */ State state;
+        /* 0x04 */ char unk04[0xC];
+    };
+    static_assert(sizeof(OutStatus) == 0x10, "MemoryPoolInfo::OutStatus has the wrong size!");
+
+    MemoryPoolInfo() = default;
+    MemoryPoolInfo(Location location_) : location{location_} {}
+
+    /**
+     * Get the CPU address for this pool.
+     *
+     * @return The CPU address of this pool.
+     */
+    CpuAddr GetCpuAddress() const;
+
+    /**
+     * Get the DSP address for this pool.
+     *
+     * @return The DSP address of this pool.
+     */
+    CpuAddr GetDspAddress() const;
+
+    /**
+     * Get the size of this pool.
+     *
+     * @return The size of this pool.
+     */
+    u64 GetSize() const;
+
+    /**
+     * Get the location of this pool.
+     *
+     * @return The location for the pool (see MemoryPoolInfo::Location).
+     */
+    Location GetLocation() const;
+
+    /**
+     * Set the CPU address for this pool.
+     *
+     * @param address - The new CPU address for this pool.
+     * @param size    - The new size for this pool.
+     */
+    void SetCpuAddress(CpuAddr address, u64 size);
+
+    /**
+     * Set the DSP address for this pool.
+     *
+     * @param address - The new DSP address for this pool.
+     */
+    void SetDspAddress(CpuAddr address);
+
+    /**
+     * Check whether the pool contains a given range.
+     *
+     * @param address - The buffer address to look for.
+     * @param size    - The size of the given buffer.
+     * @return True if the range is within this pool, otherwise false.
+     */
+    bool Contains(CpuAddr address, u64 size) const;
+
+    /**
+     * Check whether this pool is mapped, which is when the dsp address is set.
+     *
+     * @return True if the pool is mapped, otherwise false.
+     */
+    bool IsMapped() const;
+
+    /**
+     * Translates a given CPU range into a relative offset for the DSP.
+     *
+     * @param address - The buffer address to look for.
+     * @param size    - The size of the given buffer.
+     * @return Pointer to the DSP-mapped memory.
+     */
+    CpuAddr Translate(CpuAddr address, u64 size) const;
+
+    /**
+     * Set or unset whether this memory pool is in use.
+     *
+     * @param used - Use state for this pool.
+     */
+    void SetUsed(bool used);
+
+    /**
+     * Get whether this pool is in use.
+     *
+     * @return True if in use, otherwise false.
+     */
+    bool IsUsed() const;
+
+private:
+    /// Base address for the CPU-side memory
+    CpuAddr cpu_address{};
+    /// Base address for the DSP-side memory
+    CpuAddr dsp_address{};
+    /// Size of this pool
+    u64 size{};
+    /// Location of this pool, either CPU or DSP
+    Location location{Location::DSP};
+    /// If this pool is in use
+    bool in_use{};
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/memory/pool_mapper.cpp b/src/audio_core/renderer/memory/pool_mapper.cpp
new file mode 100644
index 0000000000..2baf2ce085
--- /dev/null
+++ b/src/audio_core/renderer/memory/pool_mapper.cpp
@@ -0,0 +1,243 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/memory/address_info.h"
+#include "audio_core/renderer/memory/pool_mapper.h"
+#include "core/hle/kernel/k_process.h"
+#include "core/hle/kernel/svc.h"
+
+namespace AudioCore::AudioRenderer {
+
+PoolMapper::PoolMapper(u32 process_handle_, bool force_map_)
+    : process_handle{process_handle_}, force_map{force_map_} {}
+
+PoolMapper::PoolMapper(u32 process_handle_, std::span<MemoryPoolInfo> pool_infos_, u32 pool_count_,
+                       bool force_map_)
+    : process_handle{process_handle_}, pool_infos{pool_infos_.data()},
+      pool_count{pool_count_}, force_map{force_map_} {}
+
+void PoolMapper::ClearUseState(std::span<MemoryPoolInfo> pools, const u32 count) {
+    for (u32 i = 0; i < count; i++) {
+        pools[i].SetUsed(false);
+    }
+}
+
+MemoryPoolInfo* PoolMapper::FindMemoryPool(MemoryPoolInfo* pools, const u64 count,
+                                           const CpuAddr address, const u64 size) const {
+    auto pool{pools};
+    for (u64 i = 0; i < count; i++, pool++) {
+        if (pool->Contains(address, size)) {
+            return pool;
+        }
+    }
+    return nullptr;
+}
+
+MemoryPoolInfo* PoolMapper::FindMemoryPool(const CpuAddr address, const u64 size) const {
+    auto pool{pool_infos};
+    for (u64 i = 0; i < pool_count; i++, pool++) {
+        if (pool->Contains(address, size)) {
+            return pool;
+        }
+    }
+    return nullptr;
+}
+
+bool PoolMapper::FillDspAddr(AddressInfo& address_info, MemoryPoolInfo* pools,
+                             const u32 count) const {
+    if (address_info.GetCpuAddr() == 0) {
+        address_info.SetPool(nullptr);
+        return false;
+    }
+
+    auto found_pool{
+        FindMemoryPool(pools, count, address_info.GetCpuAddr(), address_info.GetSize())};
+    if (found_pool != nullptr) {
+        address_info.SetPool(found_pool);
+        return true;
+    }
+
+    if (force_map) {
+        address_info.SetForceMappedDspAddr(address_info.GetCpuAddr());
+    } else {
+        address_info.SetPool(nullptr);
+    }
+
+    return false;
+}
+
+bool PoolMapper::FillDspAddr(AddressInfo& address_info) const {
+    if (address_info.GetCpuAddr() == 0) {
+        address_info.SetPool(nullptr);
+        return false;
+    }
+
+    auto found_pool{FindMemoryPool(address_info.GetCpuAddr(), address_info.GetSize())};
+    if (found_pool != nullptr) {
+        address_info.SetPool(found_pool);
+        return true;
+    }
+
+    if (force_map) {
+        address_info.SetForceMappedDspAddr(address_info.GetCpuAddr());
+    } else {
+        address_info.SetPool(nullptr);
+    }
+
+    return false;
+}
+
+bool PoolMapper::TryAttachBuffer(BehaviorInfo::ErrorInfo& error_info, AddressInfo& address_info,
+                                 const CpuAddr address, const u64 size) const {
+    address_info.Setup(address, size);
+
+    if (!FillDspAddr(address_info)) {
+        error_info.error_code = Service::Audio::ERR_POOL_MAPPING_FAILED;
+        error_info.address = address;
+        return force_map;
+    }
+
+    error_info.error_code = ResultSuccess;
+    error_info.address = CpuAddr(0);
+    return true;
+}
+
+bool PoolMapper::IsForceMapEnabled() const {
+    return force_map;
+}
+
+u32 PoolMapper::GetProcessHandle(const MemoryPoolInfo* pool) const {
+    switch (pool->GetLocation()) {
+    case MemoryPoolInfo::Location::CPU:
+        return process_handle;
+    case MemoryPoolInfo::Location::DSP:
+        return Kernel::Svc::CurrentProcess;
+    }
+    LOG_WARNING(Service_Audio, "Invalid MemoryPoolInfo location!");
+    return Kernel::Svc::CurrentProcess;
+}
+
+bool PoolMapper::Map([[maybe_unused]] const u32 handle, [[maybe_unused]] const CpuAddr cpu_addr,
+                     [[maybe_unused]] const u64 size) const {
+    // nn::audio::dsp::MapUserPointer(handle, cpu_addr, size);
+    return true;
+}
+
+bool PoolMapper::Map(MemoryPoolInfo& pool) const {
+    switch (pool.GetLocation()) {
+    case MemoryPoolInfo::Location::CPU:
+        // Map with process_handle
+        pool.SetDspAddress(pool.GetCpuAddress());
+        return true;
+    case MemoryPoolInfo::Location::DSP:
+        // Map with Kernel::Svc::CurrentProcess
+        pool.SetDspAddress(pool.GetCpuAddress());
+        return true;
+    default:
+        LOG_WARNING(Service_Audio, "Invalid MemoryPoolInfo location={}!",
+                    static_cast<u32>(pool.GetLocation()));
+        return false;
+    }
+}
+
+bool PoolMapper::Unmap([[maybe_unused]] const u32 handle, [[maybe_unused]] const CpuAddr cpu_addr,
+                       [[maybe_unused]] const u64 size) const {
+    // nn::audio::dsp::UnmapUserPointer(handle, cpu_addr, size);
+    return true;
+}
+
+bool PoolMapper::Unmap(MemoryPoolInfo& pool) const {
+    [[maybe_unused]] u32 handle{0};
+
+    switch (pool.GetLocation()) {
+    case MemoryPoolInfo::Location::CPU:
+        handle = process_handle;
+        break;
+    case MemoryPoolInfo::Location::DSP:
+        handle = Kernel::Svc::CurrentProcess;
+        break;
+    }
+    // nn::audio::dsp::UnmapUserPointer(handle, pool->cpu_address, pool->size);
+    pool.SetCpuAddress(0, 0);
+    pool.SetDspAddress(0);
+    return true;
+}
+
+void PoolMapper::ForceUnmapPointer(const AddressInfo& address_info) const {
+    if (force_map) {
+        [[maybe_unused]] auto found_pool{
+            FindMemoryPool(address_info.GetCpuAddr(), address_info.GetSize())};
+        // nn::audio::dsp::UnmapUserPointer(this->processHandle, address_info.GetCpuAddr(), 0);
+    }
+}
+
+MemoryPoolInfo::ResultState PoolMapper::Update(MemoryPoolInfo& pool,
+                                               const MemoryPoolInfo::InParameter& in_params,
+                                               MemoryPoolInfo::OutStatus& out_params) const {
+    if (in_params.state != MemoryPoolInfo::State::RequestAttach &&
+        in_params.state != MemoryPoolInfo::State::RequestDetach) {
+        return MemoryPoolInfo::ResultState::Success;
+    }
+
+    if (in_params.address == 0 || in_params.size == 0 || !Common::Is4KBAligned(in_params.address) ||
+        !Common::Is4KBAligned(in_params.size)) {
+        return MemoryPoolInfo::ResultState::BadParam;
+    }
+
+    switch (in_params.state) {
+    case MemoryPoolInfo::State::RequestAttach:
+        pool.SetCpuAddress(in_params.address, in_params.size);
+
+        Map(pool);
+
+        if (pool.IsMapped()) {
+            out_params.state = MemoryPoolInfo::State::Attached;
+            return MemoryPoolInfo::ResultState::Success;
+        }
+        pool.SetCpuAddress(0, 0);
+        return MemoryPoolInfo::ResultState::MapFailed;
+
+    case MemoryPoolInfo::State::RequestDetach:
+        if (pool.GetCpuAddress() != in_params.address || pool.GetSize() != in_params.size) {
+            return MemoryPoolInfo::ResultState::BadParam;
+        }
+
+        if (pool.IsUsed()) {
+            return MemoryPoolInfo::ResultState::InUse;
+        }
+
+        Unmap(pool);
+
+        pool.SetCpuAddress(0, 0);
+        pool.SetDspAddress(0);
+        out_params.state = MemoryPoolInfo::State::Detached;
+        return MemoryPoolInfo::ResultState::Success;
+
+    default:
+        LOG_ERROR(Service_Audio, "Invalid MemoryPoolInfo::State!");
+        break;
+    }
+
+    return MemoryPoolInfo::ResultState::Success;
+}
+
+bool PoolMapper::InitializeSystemPool(MemoryPoolInfo& pool, const u8* memory,
+                                      const u64 size_) const {
+    switch (pool.GetLocation()) {
+    case MemoryPoolInfo::Location::CPU:
+        return false;
+    case MemoryPoolInfo::Location::DSP:
+        pool.SetCpuAddress(reinterpret_cast<u64>(memory), size_);
+        if (Map(Kernel::Svc::CurrentProcess, reinterpret_cast<u64>(memory), size_)) {
+            pool.SetDspAddress(pool.GetCpuAddress());
+            return true;
+        }
+        return false;
+    default:
+        LOG_WARNING(Service_Audio, "Invalid MemoryPoolInfo location={}!",
+                    static_cast<u32>(pool.GetLocation()));
+        return false;
+    }
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/memory/pool_mapper.h b/src/audio_core/renderer/memory/pool_mapper.h
new file mode 100644
index 0000000000..9a691da7a2
--- /dev/null
+++ b/src/audio_core/renderer/memory/pool_mapper.h
@@ -0,0 +1,179 @@
+// 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 "common/common_types.h"
+#include "core/hle/service/audio/errors.h"
+
+namespace AudioCore::AudioRenderer {
+class AddressInfo;
+
+/**
+ * Utility functions for managing MemoryPoolInfos
+ */
+class PoolMapper {
+public:
+    explicit PoolMapper(u32 process_handle, bool force_map);
+    explicit PoolMapper(u32 process_handle, std::span<MemoryPoolInfo> pool_infos, u32 pool_count,
+                        bool force_map);
+
+    /**
+     * Clear the usage state for all given pools.
+     *
+     * @param pools - The memory pools to clear.
+     * @param count - The number of pools.
+     */
+    static void ClearUseState(std::span<MemoryPoolInfo> pools, u32 count);
+
+    /**
+     * Find the memory pool containing the given address and size from a given list of pools.
+     *
+     * @param pools   - The memory pools to search within.
+     * @param count   - The number of pools.
+     * @param address - The address of the region to find.
+     * @param size    - The size of the region to find.
+     * @return Pointer to the memory pool if found, otherwise nullptr.
+     */
+    MemoryPoolInfo* FindMemoryPool(MemoryPoolInfo* pools, u64 count, CpuAddr address,
+                                   u64 size) const;
+
+    /**
+     * Find the memory pool containing the given address and size from the PoolMapper's memory pool.
+     *
+     * @param address - The address of the region to find.
+     * @param size    - The size of the region to find.
+     * @return Pointer to the memory pool if found, otherwise nullptr.
+     */
+    MemoryPoolInfo* FindMemoryPool(CpuAddr address, u64 size) const;
+
+    /**
+     * Set the PoolMapper's memory pool to one in the given list of pools, which contains
+     * address_info.
+     *
+     * @param address_info - The expected region to find within pools.
+     * @param pools        - The list of pools to search within.
+     * @param count        - The number of pools given.
+     * @return True if successfully mapped, otherwise false.
+     */
+    bool FillDspAddr(AddressInfo& address_info, MemoryPoolInfo* pools, u32 count) const;
+
+    /**
+     * Set the PoolMapper's memory pool to the one containing address_info.
+     *
+     * @param address_info - The address to find the memory pool for.
+     * @return True if successfully mapped, otherwise false.
+     */
+    bool FillDspAddr(AddressInfo& address_info) const;
+
+    /**
+     * Try to attach a {address, size} region to the given address_info, and map it. Fills in the
+     * given error_info and address_info.
+     *
+     * @param error_info   - Output error info.
+     * @param address_info - Output address info, initialized with the given {address, size} and
+     *                       attempted to map.
+     * @param address      - Address of the region to map.
+     * @param size         - Size of the region to map.
+     * @return True if successfully attached, otherwise false.
+     */
+    bool TryAttachBuffer(BehaviorInfo::ErrorInfo& error_info, AddressInfo& address_info,
+                         CpuAddr address, u64 size) const;
+
+    /**
+     * Return whether force mapping is enabled.
+     *
+     * @return True if force mapping is enabled, otherwise false.
+     */
+    bool IsForceMapEnabled() const;
+
+    /**
+     * Get the process handle, depending on location.
+     *
+     * @param pool - The pool to check the location of.
+     * @return CurrentProcessHandle if location == DSP,
+     *         the PoolMapper's process_handle if location == CPU
+     */
+    u32 GetProcessHandle(const MemoryPoolInfo* pool) const;
+
+    /**
+     * Map the given region with the given handle. This is a no-op.
+     *
+     * @param handle   - The process handle to map to.
+     * @param cpu_addr - Address to map.
+     * @param size     - Size to map.
+     * @return True if successfully mapped, otherwise false.
+     */
+    bool Map(u32 handle, CpuAddr cpu_addr, u64 size) const;
+
+    /**
+     * Map the given memory pool.
+     *
+     * @param pool - The pool to map.
+     * @return True if successfully mapped, otherwise false.
+     */
+    bool Map(MemoryPoolInfo& pool) const;
+
+    /**
+     * Unmap the given region with the given handle.
+     *
+     * @param handle   - The process handle to unmap to.
+     * @param cpu_addr - Address to unmap.
+     * @param size     - Size to unmap.
+     * @return True if successfully unmapped, otherwise false.
+     */
+    bool Unmap(u32 handle, CpuAddr cpu_addr, u64 size) const;
+
+    /**
+     * Unmap the given memory pool.
+     *
+     * @param pool - The pool to unmap.
+     * @return True if successfully unmapped, otherwise false.
+     */
+    bool Unmap(MemoryPoolInfo& pool) const;
+
+    /**
+     * Forcibly unmap the given region.
+     *
+     * @param address_info - The region to unmap.
+     */
+    void ForceUnmapPointer(const AddressInfo& address_info) const;
+
+    /**
+     * Update the given memory pool.
+     *
+     * @param pool       - Pool to update.
+     * @param in_params  - Input parameters for the update.
+     * @param out_params - Output parameters for the update.
+     * @return The result of the update. See MemoryPoolInfo::ResultState
+     */
+    MemoryPoolInfo::ResultState Update(MemoryPoolInfo& pool,
+                                       const MemoryPoolInfo::InParameter& in_params,
+                                       MemoryPoolInfo::OutStatus& out_params) const;
+
+    /**
+     * Initialize the PoolMapper's memory pool.
+     *
+     * @param pool   - Input pool to initialize.
+     * @param memory - Pointer to the memory region for the pool.
+     * @param size   - Size of the memory region for the pool.
+     * @return True if initialized successfully, otherwise false.
+     */
+    bool InitializeSystemPool(MemoryPoolInfo& pool, const u8* memory, u64 size) const;
+
+private:
+    /// Process handle for this mapper, used when location == CPU
+    u32 process_handle;
+    /// List of memory pools assigned to this mapper
+    MemoryPoolInfo* pool_infos{};
+    /// The number of pools
+    u64 pool_count{};
+    /// Is forced mapping enabled
+    bool force_map;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/mix/mix_context.cpp b/src/audio_core/renderer/mix/mix_context.cpp
new file mode 100644
index 0000000000..2427c83ed6
--- /dev/null
+++ b/src/audio_core/renderer/mix/mix_context.cpp
@@ -0,0 +1,141 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <ranges>
+
+#include "audio_core/renderer/mix/mix_context.h"
+#include "audio_core/renderer/splitter/splitter_context.h"
+
+namespace AudioCore::AudioRenderer {
+
+void MixContext::Initialize(std::span<MixInfo*> sorted_mix_infos_, std::span<MixInfo> mix_infos_,
+                            const u32 count_, std::span<s32> effect_process_order_buffer_,
+                            const u32 effect_count_, std::span<u8> node_states_workbuffer,
+                            const u64 node_buffer_size, std::span<u8> edge_matrix_workbuffer,
+                            const u64 edge_matrix_size) {
+    count = count_;
+    sorted_mix_infos = sorted_mix_infos_;
+    mix_infos = mix_infos_;
+    effect_process_order_buffer = effect_process_order_buffer_;
+    effect_count = effect_count_;
+
+    if (node_states_workbuffer.size() > 0 && edge_matrix_workbuffer.size() > 0) {
+        node_states.Initialize(node_states_workbuffer, node_buffer_size, count);
+        edge_matrix.Initialize(edge_matrix_workbuffer, edge_matrix_size, count);
+    }
+
+    for (s32 i = 0; i < count; i++) {
+        sorted_mix_infos[i] = &mix_infos[i];
+    }
+}
+
+MixInfo* MixContext::GetSortedInfo(const s32 index) {
+    return sorted_mix_infos[index];
+}
+
+void MixContext::SetSortedInfo(const s32 index, MixInfo& mix_info) {
+    sorted_mix_infos[index] = &mix_info;
+}
+
+MixInfo* MixContext::GetInfo(const s32 index) {
+    return &mix_infos[index];
+}
+
+MixInfo* MixContext::GetFinalMixInfo() {
+    return &mix_infos[0];
+}
+
+s32 MixContext::GetCount() const {
+    return count;
+}
+
+void MixContext::UpdateDistancesFromFinalMix() {
+    for (s32 i = 0; i < count; i++) {
+        mix_infos[i].distance_from_final_mix = InvalidDistanceFromFinalMix;
+    }
+
+    for (s32 i = 0; i < count; i++) {
+        auto& mix_info{mix_infos[i]};
+        sorted_mix_infos[i] = &mix_info;
+
+        if (!mix_info.in_use) {
+            continue;
+        }
+
+        auto mix_id{mix_info.mix_id};
+        auto distance_to_final_mix{FinalMixId};
+
+        while (distance_to_final_mix < count) {
+            if (mix_id == FinalMixId) {
+                break;
+            }
+
+            if (mix_id == UnusedMixId) {
+                distance_to_final_mix = InvalidDistanceFromFinalMix;
+                break;
+            }
+
+            auto distance_from_final_mix{mix_infos[mix_id].distance_from_final_mix};
+            if (distance_from_final_mix != InvalidDistanceFromFinalMix) {
+                distance_to_final_mix = distance_from_final_mix + 1;
+                break;
+            }
+
+            distance_to_final_mix++;
+            mix_id = mix_infos[mix_id].dst_mix_id;
+        }
+
+        if (distance_to_final_mix >= count) {
+            distance_to_final_mix = InvalidDistanceFromFinalMix;
+        }
+        mix_info.distance_from_final_mix = distance_to_final_mix;
+    }
+}
+
+void MixContext::SortInfo() {
+    UpdateDistancesFromFinalMix();
+
+    std::ranges::sort(sorted_mix_infos, [](const MixInfo* lhs, const MixInfo* rhs) {
+        return lhs->distance_from_final_mix > rhs->distance_from_final_mix;
+    });
+
+    CalcMixBufferOffset();
+}
+
+void MixContext::CalcMixBufferOffset() {
+    s16 offset{0};
+    for (s32 i = 0; i < count; i++) {
+        auto mix_info{sorted_mix_infos[i]};
+        if (mix_info->in_use) {
+            const auto buffer_count{mix_info->buffer_count};
+            mix_info->buffer_offset = offset;
+            offset += buffer_count;
+        }
+    }
+}
+
+bool MixContext::TSortInfo(const SplitterContext& splitter_context) {
+    if (!splitter_context.UsingSplitter()) {
+        CalcMixBufferOffset();
+        return true;
+    }
+
+    if (!node_states.Tsort(edge_matrix)) {
+        return false;
+    }
+
+    std::vector<s32> sorted_results{node_states.GetSortedResuls()};
+    const auto result_size{std::min(count, static_cast<s32>(sorted_results.size()))};
+    for (s32 i = 0; i < result_size; i++) {
+        sorted_mix_infos[i] = &mix_infos[sorted_results[i]];
+    }
+
+    CalcMixBufferOffset();
+    return true;
+}
+
+EdgeMatrix& MixContext::GetEdgeMatrix() {
+    return edge_matrix;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/mix/mix_context.h b/src/audio_core/renderer/mix/mix_context.h
new file mode 100644
index 0000000000..da3aa28291
--- /dev/null
+++ b/src/audio_core/renderer/mix/mix_context.h
@@ -0,0 +1,124 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <span>
+
+#include "audio_core/renderer/mix/mix_info.h"
+#include "audio_core/renderer/nodes/edge_matrix.h"
+#include "audio_core/renderer/nodes/node_states.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+class SplitterContext;
+
+/*
+ * Manages mixing states, sorting and building a node graph to describe a mix order.
+ */
+class MixContext {
+public:
+    /**
+     * Initialize the mix context.
+     *
+     * @param sorted_mix_infos - Buffer for the sorted mix infos.
+     * @param mix_infos - Buffer for the mix infos.
+     * @param effect_process_order_buffer - Buffer for the effect process orders.
+     * @param effect_count - Number of effects in the buffer.
+     * @param node_states_workbuffer - Buffer for node states.
+     * @param node_buffer_size - Size of the node states buffer.
+     * @param edge_matrix_workbuffer - Buffer for edge matrix.
+     * @param edge_matrix_size - Size of the edge matrix buffer.
+     */
+    void Initialize(std::span<MixInfo*> sorted_mix_infos, std::span<MixInfo> mix_infos, u32 count_,
+                    std::span<s32> effect_process_order_buffer, u32 effect_count,
+                    std::span<u8> node_states_workbuffer, u64 node_buffer_size,
+                    std::span<u8> edge_matrix_workbuffer, u64 edge_matrix_size);
+
+    /**
+     * Get a sorted mix at the given index.
+     *
+     * @param index - Index of sorted mix.
+     * @return The sorted mix.
+     */
+    MixInfo* GetSortedInfo(s32 index);
+
+    /**
+     * Set the sorted info at the given index.
+     *
+     * @param index    - Index of sorted mix.
+     * @param mix_info - The new mix for this index.
+     */
+    void SetSortedInfo(s32 index, MixInfo& mix_info);
+
+    /**
+     * Get a mix at the given index.
+     *
+     * @param index - Index of mix.
+     * @return The mix.
+     */
+    MixInfo* GetInfo(s32 index);
+
+    /**
+     * Get the final mix.
+     *
+     * @return The final mix.
+     */
+    MixInfo* GetFinalMixInfo();
+
+    /**
+     * Get the current number of mixes.
+     *
+     * @return The number of active mixes.
+     */
+    s32 GetCount() const;
+
+    /**
+     * Update all of the mixes' distance from the final mix.
+     * Needs to be called after altering the mix graph.
+     */
+    void UpdateDistancesFromFinalMix();
+
+    /**
+     * Non-splitter sort, sorts the sorted mixes based on their distance from the final mix.
+     */
+    void SortInfo();
+
+    /**
+     * Re-calculate the mix buffer offsets for each mix after altering the mix.
+     */
+    void CalcMixBufferOffset();
+
+    /**
+     * Splitter sort, traverse the splitter node graph and sort the sorted mixes from results.
+     *
+     * @param splitter_context - Splitter context for the sort.
+     * @return True if the sort was successful, othewise false.
+     */
+    bool TSortInfo(const SplitterContext& splitter_context);
+
+    /**
+     * Get the edge matrix used for the mix graph.
+     *
+     * @return The edge matrix used.
+     */
+    EdgeMatrix& GetEdgeMatrix();
+
+private:
+    /// Array of sorted mixes
+    std::span<MixInfo*> sorted_mix_infos{};
+    /// Array of mixes
+    std::span<MixInfo> mix_infos{};
+    /// Number of active mixes
+    s32 count{};
+    /// Array of effect process orderings
+    std::span<s32> effect_process_order_buffer{};
+    /// Number of effects in the process ordering buffer
+    u64 effect_count{};
+    /// Node states used in splitter sort
+    NodeStates node_states{};
+    /// Edge matrix for connected nodes used in splitter sort
+    EdgeMatrix edge_matrix{};
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/mix/mix_info.cpp b/src/audio_core/renderer/mix/mix_info.cpp
new file mode 100644
index 0000000000..cc18e57eef
--- /dev/null
+++ b/src/audio_core/renderer/mix/mix_info.cpp
@@ -0,0 +1,120 @@
+// 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/effect/effect_context.h"
+#include "audio_core/renderer/mix/mix_info.h"
+#include "audio_core/renderer/nodes/edge_matrix.h"
+#include "audio_core/renderer/splitter/splitter_context.h"
+
+namespace AudioCore::AudioRenderer {
+
+MixInfo::MixInfo(std::span<s32> effect_order_buffer_, s32 effect_count_, BehaviorInfo& behavior)
+    : effect_order_buffer{effect_order_buffer_}, effect_count{effect_count_},
+      long_size_pre_delay_supported{behavior.IsLongSizePreDelaySupported()} {
+    ClearEffectProcessingOrder();
+}
+
+void MixInfo::Cleanup() {
+    mix_id = UnusedMixId;
+    dst_mix_id = UnusedMixId;
+    dst_splitter_id = UnusedSplitterId;
+}
+
+void MixInfo::ClearEffectProcessingOrder() {
+    for (s32 i = 0; i < effect_count; i++) {
+        effect_order_buffer[i] = -1;
+    }
+}
+
+bool MixInfo::Update(EdgeMatrix& edge_matrix, const InParameter& in_params,
+                     EffectContext& effect_context, SplitterContext& splitter_context,
+                     const BehaviorInfo& behavior) {
+    volume = in_params.volume;
+    sample_rate = in_params.sample_rate;
+    buffer_count = static_cast<s16>(in_params.buffer_count);
+    in_use = in_params.in_use;
+    mix_id = in_params.mix_id;
+    node_id = in_params.node_id;
+    mix_volumes = in_params.mix_volumes;
+
+    bool sort_required{false};
+    if (behavior.IsSplitterSupported()) {
+        sort_required = UpdateConnection(edge_matrix, in_params, splitter_context);
+    } else {
+        if (dst_mix_id != in_params.dest_mix_id) {
+            dst_mix_id = in_params.dest_mix_id;
+            sort_required = true;
+        }
+        dst_splitter_id = UnusedSplitterId;
+    }
+
+    ClearEffectProcessingOrder();
+
+    // Check all effects, and set their order if they belong to this mix.
+    const auto count{effect_context.GetCount()};
+    for (u32 i = 0; i < count; i++) {
+        const auto& info{effect_context.GetInfo(i)};
+        if (mix_id == info.GetMixId()) {
+            const auto processing_order{info.GetProcessingOrder()};
+            if (processing_order > effect_count) {
+                break;
+            }
+            effect_order_buffer[processing_order] = i;
+        }
+    }
+
+    return sort_required;
+}
+
+bool MixInfo::UpdateConnection(EdgeMatrix& edge_matrix, const InParameter& in_params,
+                               SplitterContext& splitter_context) {
+    auto has_new_connection{false};
+    if (dst_splitter_id != UnusedSplitterId) {
+        auto& splitter_info{splitter_context.GetInfo(dst_splitter_id)};
+        has_new_connection = splitter_info.HasNewConnection();
+    }
+
+    // Check if this mix matches the input parameters.
+    // If everything is the same, don't bother updating.
+    if (dst_mix_id == in_params.dest_mix_id && dst_splitter_id == in_params.dest_splitter_id &&
+        !has_new_connection) {
+        return false;
+    }
+
+    // Reset the mix in the graph, as we're about to update it.
+    edge_matrix.RemoveEdges(mix_id);
+
+    if (in_params.dest_mix_id == UnusedMixId) {
+        if (in_params.dest_splitter_id != UnusedSplitterId) {
+            // If the splitter is used, connect this mix to each active destination.
+            auto& splitter_info{splitter_context.GetInfo(in_params.dest_splitter_id)};
+            auto const destination_count{splitter_info.GetDestinationCount()};
+
+            for (u32 i = 0; i < destination_count; i++) {
+                auto destination{
+                    splitter_context.GetDesintationData(in_params.dest_splitter_id, i)};
+
+                if (destination) {
+                    const auto destination_id{destination->GetMixId()};
+                    if (destination_id != UnusedMixId) {
+                        edge_matrix.Connect(mix_id, destination_id);
+                    }
+                }
+            }
+        }
+    } else {
+        // If the splitter is not used, only connect this mix to its destination.
+        edge_matrix.Connect(mix_id, in_params.dest_mix_id);
+    }
+
+    dst_mix_id = in_params.dest_mix_id;
+    dst_splitter_id = in_params.dest_splitter_id;
+    return true;
+}
+
+bool MixInfo::HasAnyConnection() const {
+    return dst_mix_id != UnusedMixId || dst_splitter_id != UnusedSplitterId;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/mix/mix_info.h b/src/audio_core/renderer/mix/mix_info.h
new file mode 100644
index 0000000000..b5fa4c0c7e
--- /dev/null
+++ b/src/audio_core/renderer/mix/mix_info.h
@@ -0,0 +1,124 @@
+// 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"
+
+namespace AudioCore::AudioRenderer {
+class EdgeMatrix;
+class SplitterContext;
+class EffectContext;
+class BehaviorInfo;
+
+/**
+ * A single mix, which may feed through other mixes in a chain until reaching the final output mix.
+ */
+class MixInfo {
+public:
+    struct InParameter {
+        /* 0x000 */ f32 volume;
+        /* 0x004 */ u32 sample_rate;
+        /* 0x008 */ u32 buffer_count;
+        /* 0x00C */ bool in_use;
+        /* 0x00D */ bool is_dirty;
+        /* 0x010 */ s32 mix_id;
+        /* 0x014 */ u32 effect_count;
+        /* 0x018 */ s32 node_id;
+        /* 0x01C */ char unk01C[0x8];
+        /* 0x024 */ std::array<std::array<f32, MaxMixBuffers>, MaxMixBuffers> mix_volumes;
+        /* 0x924 */ s32 dest_mix_id;
+        /* 0x928 */ s32 dest_splitter_id;
+        /* 0x92C */ char unk92C[0x4];
+    };
+    static_assert(sizeof(InParameter) == 0x930, "MixInfo::InParameter has the wrong size!");
+
+    struct InDirtyParameter {
+        /* 0x00 */ u32 magic;
+        /* 0x04 */ s32 count;
+        /* 0x08 */ char unk08[0x18];
+    };
+    static_assert(sizeof(InDirtyParameter) == 0x20,
+                  "MixInfo::InDirtyParameter has the wrong size!");
+
+    MixInfo(std::span<s32> effect_order_buffer, s32 effect_count, BehaviorInfo& behavior);
+
+    /**
+     * Clean up the mix, resetting it to a default state.
+     */
+    void Cleanup();
+
+    /**
+     * Clear the effect process order for all effects in this mix.
+     */
+    void ClearEffectProcessingOrder();
+
+    /**
+     * Update the mix according to the given parameters.
+     *
+     * @param edge_matrix      - Updated with new splitter node connections, if supported.
+     * @param in_params        - Input parameters.
+     * @param effect_context   - Used to update the effect orderings.
+     * @param splitter_context - Used to update the mix graph if supported.
+     * @param behavior        - Used for checking which features are supported.
+     * @return True if the mix was updated and a sort is required, otherwise false.
+     */
+    bool Update(EdgeMatrix& edge_matrix, const InParameter& in_params,
+                EffectContext& effect_context, SplitterContext& splitter_context,
+                const BehaviorInfo& behavior);
+
+    /**
+     * Update the mix's connection in the node graph according to the given parameters.
+     *
+     * @param edge_matrix      - Updated with new splitter node connections, if supported.
+     * @param in_params        - Input parameters.
+     * @param splitter_context - Used to update the mix graph if supported.
+     * @return True if the mix was updated and a sort is required, otherwise false.
+     */
+    bool UpdateConnection(EdgeMatrix& edge_matrix, const InParameter& in_params,
+                          SplitterContext& splitter_context);
+
+    /**
+     * Check if this mix is connected to any other.
+     *
+     * @return True if the mix has a connection, otherwise false.
+     */
+    bool HasAnyConnection() const;
+
+    /// Volume of this mix
+    f32 volume{};
+    /// Sample rate of this mix
+    u32 sample_rate{};
+    /// Number of buffers in this mix
+    s16 buffer_count{};
+    /// Is this mix in use?
+    bool in_use{};
+    /// Is this mix enabled?
+    bool enabled{};
+    /// Id of this mix
+    s32 mix_id{UnusedMixId};
+    /// Node id of this mix
+    s32 node_id{};
+    /// Buffer offset for this mix
+    s16 buffer_offset{};
+    /// Distance to the final mix
+    s32 distance_from_final_mix{InvalidDistanceFromFinalMix};
+    /// Array of effect orderings of all effects in this mix
+    std::span<s32> effect_order_buffer;
+    /// Number of effects in this mix
+    const s32 effect_count;
+    /// Id for next mix in the chain
+    s32 dst_mix_id{UnusedMixId};
+    /// Mixing volumes for this mix used when this mix is chained with another
+    std::array<std::array<f32, MaxMixBuffers>, MaxMixBuffers> mix_volumes{};
+    /// Id for next mix in the graph when splitter is used
+    s32 dst_splitter_id{UnusedSplitterId};
+    /// Is a longer pre-delay time supported for the reverb effect?
+    const bool long_size_pre_delay_supported;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/nodes/bit_array.h b/src/audio_core/renderer/nodes/bit_array.h
new file mode 100644
index 0000000000..b0d53cd512
--- /dev/null
+++ b/src/audio_core/renderer/nodes/bit_array.h
@@ -0,0 +1,25 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <vector>
+
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * Represents an array of bits used for nodes and edges for the mixing graph.
+ */
+struct BitArray {
+    void reset() {
+        buffer.assign(buffer.size(), false);
+    }
+
+    /// Bits
+    std::vector<bool> buffer{};
+    /// Size of the buffer
+    u32 size{};
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/nodes/edge_matrix.cpp b/src/audio_core/renderer/nodes/edge_matrix.cpp
new file mode 100644
index 0000000000..5573f33b97
--- /dev/null
+++ b/src/audio_core/renderer/nodes/edge_matrix.cpp
@@ -0,0 +1,38 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/nodes/edge_matrix.h"
+
+namespace AudioCore::AudioRenderer {
+
+void EdgeMatrix::Initialize([[maybe_unused]] std::span<u8> buffer,
+                            [[maybe_unused]] const u64 node_buffer_size, const u32 count_) {
+    count = count_;
+    edges.buffer.resize(count_ * count_);
+    edges.size = count_ * count_;
+    edges.reset();
+}
+
+bool EdgeMatrix::Connected(const u32 id, const u32 destination_id) const {
+    return edges.buffer[count * id + destination_id];
+}
+
+void EdgeMatrix::Connect(const u32 id, const u32 destination_id) {
+    edges.buffer[count * id + destination_id] = true;
+}
+
+void EdgeMatrix::Disconnect(const u32 id, const u32 destination_id) {
+    edges.buffer[count * id + destination_id] = false;
+}
+
+void EdgeMatrix::RemoveEdges(const u32 id) {
+    for (u32 dest = 0; dest < count; dest++) {
+        Disconnect(id, dest);
+    }
+}
+
+u32 EdgeMatrix::GetNodeCount() const {
+    return count;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/nodes/edge_matrix.h b/src/audio_core/renderer/nodes/edge_matrix.h
new file mode 100644
index 0000000000..27a20e43ec
--- /dev/null
+++ b/src/audio_core/renderer/nodes/edge_matrix.h
@@ -0,0 +1,82 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <span>
+
+#include "audio_core/renderer/nodes/bit_array.h"
+#include "common/alignment.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * An edge matrix, holding the connections for each node to every other node in the graph.
+ */
+class EdgeMatrix {
+public:
+    /**
+     * Calculate the size required for its workbuffer.
+     *
+     * @param count - The number of nodes in the graph.
+     * @return The required workbuffer size.
+     */
+    static u64 GetWorkBufferSize(u32 count) {
+        return Common::AlignUp(count * count, 0x40) / sizeof(u64);
+    }
+
+    /**
+     * Initialize this edge matrix.
+     *
+     * @param buffer           - The workbuffer to use. Unused.
+     * @param node_buffer_size - The size of the workbuffer. Unused.
+     * @param count            - The number of nodes in the graph.
+     */
+    void Initialize(std::span<u8> buffer, u64 node_buffer_size, u32 count);
+
+    /**
+     * Check if a node is connected to another.
+     *
+     * @param id             - The node id to check.
+     * @param destination_id - Node id to check connection with.
+     */
+    bool Connected(u32 id, u32 destination_id) const;
+
+    /**
+     * Connect a node to another.
+     *
+     * @param id             - The node id to connect.
+     * @param destination_id - Destination to connect it to.
+     */
+    void Connect(u32 id, u32 destination_id);
+
+    /**
+     * Disconnect a node from another.
+     *
+     * @param id             - The node id to disconnect.
+     * @param destination_id - Destination to disconnect it from.
+     */
+    void Disconnect(u32 id, u32 destination_id);
+
+    /**
+     * Remove all connections for a given node.
+     *
+     * @param id - The node id to disconnect.
+     */
+    void RemoveEdges(u32 id);
+
+    /**
+     * Get the number of nodes in the graph.
+     *
+     * @return Number of nodes.
+     */
+    u32 GetNodeCount() const;
+
+private:
+    /// Edges for the current graph
+    BitArray edges;
+    /// Number of nodes (not edges) in the graph
+    u32 count;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/nodes/node_states.cpp b/src/audio_core/renderer/nodes/node_states.cpp
new file mode 100644
index 0000000000..1821a51e66
--- /dev/null
+++ b/src/audio_core/renderer/nodes/node_states.cpp
@@ -0,0 +1,141 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/nodes/node_states.h"
+#include "common/logging/log.h"
+
+namespace AudioCore::AudioRenderer {
+
+void NodeStates::Initialize(std::span<u8> buffer_, [[maybe_unused]] const u64 node_buffer_size,
+                            const u32 count) {
+    u64 num_blocks{Common::AlignUp(count, 0x40) / sizeof(u64)};
+    u64 offset{0};
+
+    node_count = count;
+
+    nodes_found.buffer.resize(count);
+    nodes_found.size = count;
+    nodes_found.reset();
+
+    offset += num_blocks;
+
+    nodes_complete.buffer.resize(count);
+    nodes_complete.size = count;
+    nodes_complete.reset();
+
+    offset += num_blocks;
+
+    results = {reinterpret_cast<u32*>(&buffer_[offset]), count};
+
+    offset += count * sizeof(u32);
+
+    stack.stack = {reinterpret_cast<u32*>(&buffer_[offset]), count * count};
+    stack.size = count * count;
+    stack.unk_10 = count * count;
+
+    offset += count * count * sizeof(u32);
+}
+
+bool NodeStates::Tsort(const EdgeMatrix& edge_matrix) {
+    return DepthFirstSearch(edge_matrix, stack);
+}
+
+bool NodeStates::DepthFirstSearch(const EdgeMatrix& edge_matrix, Stack& stack_) {
+    ResetState();
+
+    for (u32 node_id = 0; node_id < node_count; node_id++) {
+        if (GetState(node_id) == SearchState::Unknown) {
+            stack_.push(node_id);
+        }
+
+        while (stack_.Count() > 0) {
+            auto current_node{stack_.top()};
+            switch (GetState(current_node)) {
+            case SearchState::Unknown:
+                SetState(current_node, SearchState::Found);
+                break;
+            case SearchState::Found:
+                SetState(current_node, SearchState::Complete);
+                PushTsortResult(current_node);
+                stack_.pop();
+                continue;
+            case SearchState::Complete:
+                stack_.pop();
+                continue;
+            }
+
+            const auto edge_count{edge_matrix.GetNodeCount()};
+            for (u32 edge_id = 0; edge_id < edge_count; edge_id++) {
+                if (!edge_matrix.Connected(current_node, edge_id)) {
+                    continue;
+                }
+
+                switch (GetState(edge_id)) {
+                case SearchState::Unknown:
+                    stack_.push(edge_id);
+                    break;
+                case SearchState::Found:
+                    LOG_ERROR(Service_Audio,
+                              "Cycle detected in the node graph, graph is not a DAG! "
+                              "Bailing to avoid an infinite loop");
+                    ResetState();
+                    return false;
+                case SearchState::Complete:
+                    break;
+                }
+            }
+        }
+    }
+
+    return true;
+}
+
+NodeStates::SearchState NodeStates::GetState(const u32 id) const {
+    if (nodes_found.buffer[id]) {
+        return SearchState::Found;
+    } else if (nodes_complete.buffer[id]) {
+        return SearchState::Complete;
+    }
+    return SearchState::Unknown;
+}
+
+void NodeStates::PushTsortResult(const u32 id) {
+    results[result_pos++] = id;
+}
+
+void NodeStates::SetState(const u32 id, const SearchState state) {
+    switch (state) {
+    case SearchState::Complete:
+        nodes_found.buffer[id] = false;
+        nodes_complete.buffer[id] = true;
+        break;
+    case SearchState::Found:
+        nodes_found.buffer[id] = true;
+        nodes_complete.buffer[id] = false;
+        break;
+    case SearchState::Unknown:
+        nodes_found.buffer[id] = false;
+        nodes_complete.buffer[id] = false;
+        break;
+    default:
+        LOG_ERROR(Service_Audio, "Unknown node SearchState {}", static_cast<u32>(state));
+        break;
+    }
+}
+
+void NodeStates::ResetState() {
+    nodes_found.reset();
+    nodes_complete.reset();
+    std::fill(results.begin(), results.end(), -1);
+    result_pos = 0;
+}
+
+u32 NodeStates::GetNodeCount() const {
+    return node_count;
+}
+
+std::vector<s32> NodeStates::GetSortedResuls() const {
+    return {results.rbegin(), results.rbegin() + result_pos};
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/nodes/node_states.h b/src/audio_core/renderer/nodes/node_states.h
new file mode 100644
index 0000000000..a1e0958a29
--- /dev/null
+++ b/src/audio_core/renderer/nodes/node_states.h
@@ -0,0 +1,195 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <span>
+#include <vector>
+
+#include "audio_core/renderer/nodes/edge_matrix.h"
+#include "common/alignment.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * Graph utility functions for sorting and getting results from the DAG.
+ */
+class NodeStates {
+    /**
+     * State of a node in the depth first search.
+     */
+    enum class SearchState {
+        Unknown,
+        Found,
+        Complete,
+    };
+
+    /**
+     * Stack used for a depth first search.
+     */
+    struct Stack {
+        /**
+         * Calculate the workbuffer size required for this stack.
+         *
+         * @param count - Maximum number of nodes for the stack.
+         * @return Required buffer size.
+         */
+        static u32 CalcBufferSize(u32 count) {
+            return count * sizeof(u32);
+        }
+
+        /**
+         * Reset the stack back to default.
+         *
+         * @param buffer_ - The new buffer to use.
+         * @param size_   - The size of the new buffer.
+         */
+        void Reset(u32* buffer_, u32 size_) {
+            stack = {buffer_, size_};
+            size = size_;
+            pos = 0;
+            unk_10 = size_;
+        }
+
+        /**
+         * Get the current stack position.
+         *
+         * @return The current stack position.
+         */
+        u32 Count() {
+            return pos;
+        }
+
+        /**
+         * Push a new node to the stack.
+         *
+         * @param data - The node to push.
+         */
+        void push(u32 data) {
+            stack[pos++] = data;
+        }
+
+        /**
+         * Pop a node from the stack.
+         *
+         * @return The node on the top of the stack.
+         */
+        u32 pop() {
+            return stack[--pos];
+        }
+
+        /**
+         * Get the top of the stack without popping.
+         *
+         * @return The node on the top of the stack.
+         */
+        u32 top() {
+            return stack[pos - 1];
+        }
+
+        /// Buffer for the stack
+        std::span<u32> stack{};
+        /// Size of the stack buffer
+        u32 size{};
+        /// Current stack position
+        u32 pos{};
+        /// Unknown
+        u32 unk_10{};
+    };
+
+public:
+    /**
+     * Calculate the workbuffer size required for the node states.
+     *
+     * @param count - The number of nodes.
+     * @return The required workbuffer size.
+     */
+    static u64 GetWorkBufferSize(u32 count) {
+        return (Common::AlignUp(count, 0x40) / sizeof(u64)) * 2 + count * sizeof(BitArray) +
+               count * Stack::CalcBufferSize(count);
+    }
+
+    /**
+     * Initialize the node states.
+     *
+     * @param buffer           - The workbuffer to use. Unused.
+     * @param node_buffer_size - The size of the workbuffer. Unused.
+     * @param count            - The number of nodes in the graph.
+     */
+    void Initialize(std::span<u8> nodes, u64 node_buffer_size, u32 count);
+
+    /**
+     * Sort the graph. Only calls DepthFirstSearch.
+     *
+     * @param edge_matrix - The edge matrix used to hold the connections between nodes.
+     * @return True if the sort was successful, otherwise false.
+     */
+    bool Tsort(const EdgeMatrix& edge_matrix);
+
+    /**
+     * Sort the graph via depth first search.
+     *
+     * @param edge_matrix - The edge matrix used to hold the connections between nodes.
+     * @param stack       - The stack used for pushing and popping nodes.
+     * @return True if the sort was successful, otherwise false.
+     */
+    bool DepthFirstSearch(const EdgeMatrix& edge_matrix, Stack& stack);
+
+    /**
+     * Get the search state of a given node.
+     *
+     * @param id - The node id to check.
+     * @return The node's search state. See SearchState
+     */
+    SearchState GetState(u32 id) const;
+
+    /**
+     * Push a node id to the results buffer when found in the DFS.
+     *
+     * @param id - The node id to push.
+     */
+    void PushTsortResult(u32 id);
+
+    /**
+     * Set the state of a node.
+     *
+     * @param id - The node id to alter.
+     * @param state - The new search state.
+     */
+    void SetState(u32 id, SearchState state);
+
+    /**
+     * Reset the nodes found, complete and the results.
+     */
+    void ResetState();
+
+    /**
+     * Get the number of nodes in the graph.
+     *
+     * @return The number of nodes.
+     */
+    u32 GetNodeCount() const;
+
+    /**
+     * Get the sorted results from the DFS.
+     *
+     * @return Vector of nodes in reverse order.
+     */
+    std::vector<s32> GetSortedResuls() const;
+
+private:
+    /// Number of nodes in the graph
+    u32 node_count{};
+    /// Position in results buffer
+    u32 result_pos{};
+    /// List of nodes found
+    BitArray nodes_found{};
+    /// List of nodes completed
+    BitArray nodes_complete{};
+    /// List of results from the depth first search
+    std::span<u32> results{};
+    /// Stack used during the depth first search
+    Stack stack{};
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/performance/detail_aspect.cpp b/src/audio_core/renderer/performance/detail_aspect.cpp
new file mode 100644
index 0000000000..f6405937fd
--- /dev/null
+++ b/src/audio_core/renderer/performance/detail_aspect.cpp
@@ -0,0 +1,25 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/command/command_buffer.h"
+#include "audio_core/renderer/command/command_generator.h"
+#include "audio_core/renderer/performance/detail_aspect.h"
+
+namespace AudioCore::AudioRenderer {
+
+DetailAspect::DetailAspect(CommandGenerator& command_generator_,
+                           const PerformanceEntryType entry_type, const s32 node_id_,
+                           const PerformanceDetailType detail_type)
+    : command_generator{command_generator_}, node_id{node_id_} {
+    auto perf_manager{command_generator.GetPerformanceManager()};
+    if (perf_manager != nullptr && perf_manager->IsInitialized() &&
+        perf_manager->IsDetailTarget(node_id) &&
+        perf_manager->GetNextEntry(performance_entry_address, detail_type, entry_type, node_id)) {
+        command_generator.GeneratePerformanceCommand(node_id, PerformanceState::Start,
+                                                     performance_entry_address);
+
+        initialized = true;
+    }
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/performance/detail_aspect.h b/src/audio_core/renderer/performance/detail_aspect.h
new file mode 100644
index 0000000000..ee4ac2f766
--- /dev/null
+++ b/src/audio_core/renderer/performance/detail_aspect.h
@@ -0,0 +1,33 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "audio_core/renderer/performance/performance_entry_addresses.h"
+#include "audio_core/renderer/performance/performance_manager.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+class CommandGenerator;
+
+/**
+ * Holds detailed information about performance metrics, filled in by the AudioRenderer during
+ * Performance commands.
+ */
+class DetailAspect {
+public:
+    DetailAspect() = default;
+    DetailAspect(CommandGenerator& command_generator, PerformanceEntryType entry_type, s32 node_id,
+                 PerformanceDetailType detail_type);
+
+    /// Command generator the command will be generated into
+    CommandGenerator& command_generator;
+    /// Addresses to be filled by the AudioRenderer
+    PerformanceEntryAddresses performance_entry_address{};
+    /// Is this detail aspect initialized?
+    bool initialized{};
+    /// Node id of this aspect
+    s32 node_id;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/performance/entry_aspect.cpp b/src/audio_core/renderer/performance/entry_aspect.cpp
new file mode 100644
index 0000000000..dd4165803c
--- /dev/null
+++ b/src/audio_core/renderer/performance/entry_aspect.cpp
@@ -0,0 +1,23 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/command/command_buffer.h"
+#include "audio_core/renderer/command/command_generator.h"
+#include "audio_core/renderer/performance/entry_aspect.h"
+
+namespace AudioCore::AudioRenderer {
+
+EntryAspect::EntryAspect(CommandGenerator& command_generator_, const PerformanceEntryType type,
+                         const s32 node_id_)
+    : command_generator{command_generator_}, node_id{node_id_} {
+    auto perf_manager{command_generator.GetPerformanceManager()};
+    if (perf_manager != nullptr && perf_manager->IsInitialized() &&
+        perf_manager->GetNextEntry(performance_entry_address, type, node_id)) {
+        command_generator.GeneratePerformanceCommand(node_id, PerformanceState::Start,
+                                                     performance_entry_address);
+
+        initialized = true;
+    }
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/performance/entry_aspect.h b/src/audio_core/renderer/performance/entry_aspect.h
new file mode 100644
index 0000000000..01c1eb3f15
--- /dev/null
+++ b/src/audio_core/renderer/performance/entry_aspect.h
@@ -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/performance/performance_entry_addresses.h"
+#include "audio_core/renderer/performance/performance_manager.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+class CommandGenerator;
+
+/**
+ * Holds entry information about performance metrics, filled in by the AudioRenderer during
+ * Performance commands.
+ */
+class EntryAspect {
+public:
+    EntryAspect() = default;
+    EntryAspect(CommandGenerator& command_generator, PerformanceEntryType type, s32 node_id);
+
+    /// Command generator the command will be generated into
+    CommandGenerator& command_generator;
+    /// Addresses to be filled by the AudioRenderer
+    PerformanceEntryAddresses performance_entry_address{};
+    /// Is this detail aspect initialized?
+    bool initialized{};
+    /// Node id of this aspect
+    s32 node_id;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/performance/performance_detail.h b/src/audio_core/renderer/performance/performance_detail.h
new file mode 100644
index 0000000000..3a4897e606
--- /dev/null
+++ b/src/audio_core/renderer/performance/performance_detail.h
@@ -0,0 +1,50 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "audio_core/renderer/performance/performance_entry.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+
+enum class PerformanceDetailType : u8 {
+    Invalid,
+    Unk1,
+    Unk2,
+    Unk3,
+    Unk4,
+    Unk5,
+    Unk6,
+    Unk7,
+    Unk8,
+    Unk9,
+    Unk10,
+    Unk11,
+    Unk12,
+    Unk13,
+};
+
+struct PerformanceDetailVersion1 {
+    /* 0x00 */ u32 node_id;
+    /* 0x04 */ u32 start_time;
+    /* 0x08 */ u32 processed_time;
+    /* 0x0C */ PerformanceDetailType detail_type;
+    /* 0x0D */ PerformanceEntryType entry_type;
+};
+static_assert(sizeof(PerformanceDetailVersion1) == 0x10,
+              "PerformanceDetailVersion1 has the worng size!");
+
+struct PerformanceDetailVersion2 {
+    /* 0x00 */ u32 node_id;
+    /* 0x04 */ u32 start_time;
+    /* 0x08 */ u32 processed_time;
+    /* 0x0C */ PerformanceDetailType detail_type;
+    /* 0x0D */ PerformanceEntryType entry_type;
+    /* 0x10 */ u32 unk_10;
+    /* 0x14 */ char unk14[0x4];
+};
+static_assert(sizeof(PerformanceDetailVersion2) == 0x18,
+              "PerformanceDetailVersion2 has the worng size!");
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/performance/performance_entry.h b/src/audio_core/renderer/performance/performance_entry.h
new file mode 100644
index 0000000000..d1b21406b5
--- /dev/null
+++ b/src/audio_core/renderer/performance/performance_entry.h
@@ -0,0 +1,37 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+
+enum class PerformanceEntryType : u8 {
+    Invalid,
+    Voice,
+    SubMix,
+    FinalMix,
+    Sink,
+};
+
+struct PerformanceEntryVersion1 {
+    /* 0x00 */ u32 node_id;
+    /* 0x04 */ u32 start_time;
+    /* 0x08 */ u32 processed_time;
+    /* 0x0C */ PerformanceEntryType entry_type;
+};
+static_assert(sizeof(PerformanceEntryVersion1) == 0x10,
+              "PerformanceEntryVersion1 has the worng size!");
+
+struct PerformanceEntryVersion2 {
+    /* 0x00 */ u32 node_id;
+    /* 0x04 */ u32 start_time;
+    /* 0x08 */ u32 processed_time;
+    /* 0x0C */ PerformanceEntryType entry_type;
+    /* 0x0D */ char unk0D[0xB];
+};
+static_assert(sizeof(PerformanceEntryVersion2) == 0x18,
+              "PerformanceEntryVersion2 has the worng size!");
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/performance/performance_entry_addresses.h b/src/audio_core/renderer/performance/performance_entry_addresses.h
new file mode 100644
index 0000000000..e381d765ce
--- /dev/null
+++ b/src/audio_core/renderer/performance/performance_entry_addresses.h
@@ -0,0 +1,17 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "audio_core/common/common.h"
+
+namespace AudioCore::AudioRenderer {
+
+struct PerformanceEntryAddresses {
+    CpuAddr translated_address;
+    CpuAddr entry_start_time_offset;
+    CpuAddr header_entry_count_offset;
+    CpuAddr entry_processed_time_offset;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/performance/performance_frame_header.h b/src/audio_core/renderer/performance/performance_frame_header.h
new file mode 100644
index 0000000000..707cc0afb2
--- /dev/null
+++ b/src/audio_core/renderer/performance/performance_frame_header.h
@@ -0,0 +1,36 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+
+struct PerformanceFrameHeaderVersion1 {
+    /* 0x00 */ u32 magic; // "PERF"
+    /* 0x04 */ u32 entry_count;
+    /* 0x08 */ u32 detail_count;
+    /* 0x0C */ u32 next_offset;
+    /* 0x10 */ u32 total_processing_time;
+    /* 0x14 */ u32 frame_index;
+};
+static_assert(sizeof(PerformanceFrameHeaderVersion1) == 0x18,
+              "PerformanceFrameHeaderVersion1 has the worng size!");
+
+struct PerformanceFrameHeaderVersion2 {
+    /* 0x00 */ u32 magic; // "PERF"
+    /* 0x04 */ u32 entry_count;
+    /* 0x08 */ u32 detail_count;
+    /* 0x0C */ u32 next_offset;
+    /* 0x10 */ u32 total_processing_time;
+    /* 0x14 */ u32 voices_dropped;
+    /* 0x18 */ u64 start_time;
+    /* 0x20 */ u32 frame_index;
+    /* 0x24 */ bool render_time_exceeded;
+    /* 0x25 */ char unk25[0xB];
+};
+static_assert(sizeof(PerformanceFrameHeaderVersion2) == 0x30,
+              "PerformanceFrameHeaderVersion2 has the worng size!");
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/performance/performance_manager.cpp b/src/audio_core/renderer/performance/performance_manager.cpp
new file mode 100644
index 0000000000..fd5873e1ed
--- /dev/null
+++ b/src/audio_core/renderer/performance/performance_manager.cpp
@@ -0,0 +1,645 @@
+// 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/memory/memory_pool_info.h"
+#include "audio_core/renderer/performance/performance_manager.h"
+#include "common/common_funcs.h"
+
+namespace AudioCore::AudioRenderer {
+
+void PerformanceManager::CreateImpl(const size_t version) {
+    switch (version) {
+    case 1:
+        impl = std::make_unique<
+            PerformanceManagerImpl<PerformanceVersion::Version1, PerformanceFrameHeaderVersion1,
+                                   PerformanceEntryVersion1, PerformanceDetailVersion1>>();
+        break;
+    case 2:
+        impl = std::make_unique<
+            PerformanceManagerImpl<PerformanceVersion::Version2, PerformanceFrameHeaderVersion2,
+                                   PerformanceEntryVersion2, PerformanceDetailVersion2>>();
+        break;
+    default:
+        LOG_WARNING(Service_Audio, "Invalid PerformanceMetricsDataFormat {}, creating version 1",
+                    static_cast<u32>(version));
+        impl = std::make_unique<
+            PerformanceManagerImpl<PerformanceVersion::Version1, PerformanceFrameHeaderVersion1,
+                                   PerformanceEntryVersion1, PerformanceDetailVersion1>>();
+    }
+}
+
+void PerformanceManager::Initialize(std::span<u8> workbuffer, const u64 workbuffer_size,
+                                    const AudioRendererParameterInternal& params,
+                                    const BehaviorInfo& behavior,
+                                    const MemoryPoolInfo& memory_pool) {
+    CreateImpl(behavior.GetPerformanceMetricsDataFormat());
+    impl->Initialize(workbuffer, workbuffer_size, params, behavior, memory_pool);
+}
+
+bool PerformanceManager::IsInitialized() const {
+    if (impl) {
+        return impl->IsInitialized();
+    }
+    return false;
+}
+
+u32 PerformanceManager::CopyHistories(u8* out_buffer, u64 out_size) {
+    if (impl) {
+        return impl->CopyHistories(out_buffer, out_size);
+    }
+    return 0;
+}
+
+bool PerformanceManager::GetNextEntry(PerformanceEntryAddresses& addresses, u32** unk,
+                                      const PerformanceSysDetailType sys_detail_type,
+                                      const s32 node_id) {
+    if (impl) {
+        return impl->GetNextEntry(addresses, unk, sys_detail_type, node_id);
+    }
+    return false;
+}
+
+bool PerformanceManager::GetNextEntry(PerformanceEntryAddresses& addresses,
+                                      const PerformanceEntryType entry_type, const s32 node_id) {
+    if (impl) {
+        return impl->GetNextEntry(addresses, entry_type, node_id);
+    }
+    return false;
+}
+
+bool PerformanceManager::GetNextEntry(PerformanceEntryAddresses& addresses,
+                                      const PerformanceDetailType detail_type,
+                                      const PerformanceEntryType entry_type, const s32 node_id) {
+    if (impl) {
+        return impl->GetNextEntry(addresses, detail_type, entry_type, node_id);
+    }
+    return false;
+}
+
+void PerformanceManager::TapFrame(const bool dsp_behind, const u32 voices_dropped,
+                                  const u64 rendering_start_tick) {
+    if (impl) {
+        impl->TapFrame(dsp_behind, voices_dropped, rendering_start_tick);
+    }
+}
+
+bool PerformanceManager::IsDetailTarget(const u32 target_node_id) const {
+    if (impl) {
+        return impl->IsDetailTarget(target_node_id);
+    }
+    return false;
+}
+
+void PerformanceManager::SetDetailTarget(const u32 target_node_id) {
+    if (impl) {
+        impl->SetDetailTarget(target_node_id);
+    }
+}
+
+template <>
+void PerformanceManagerImpl<
+    PerformanceVersion::Version1, PerformanceFrameHeaderVersion1, PerformanceEntryVersion1,
+    PerformanceDetailVersion1>::Initialize(std::span<u8> workbuffer_, const u64 workbuffer_size,
+                                           const AudioRendererParameterInternal& params,
+                                           const BehaviorInfo& behavior,
+                                           const MemoryPoolInfo& memory_pool) {
+    workbuffer = workbuffer_;
+    entries_per_frame = params.voices + params.effects + params.sinks + params.sub_mixes + 1;
+    max_detail_count = MaxDetailEntries;
+    frame_size = GetRequiredBufferSizeForPerformanceMetricsPerFrame(behavior, params);
+    const auto frame_count{static_cast<u32>(workbuffer_size / frame_size)};
+    max_frames = frame_count - 1;
+    translated_buffer = memory_pool.Translate(CpuAddr(workbuffer.data()), workbuffer_size);
+
+    // The first frame is the "current" frame we're writing to.
+    auto buffer_offset{workbuffer.data()};
+    frame_header = reinterpret_cast<PerformanceFrameHeaderVersion1*>(buffer_offset);
+    buffer_offset += sizeof(PerformanceFrameHeaderVersion1);
+    entry_buffer = {reinterpret_cast<PerformanceEntryVersion1*>(buffer_offset), entries_per_frame};
+    buffer_offset += entries_per_frame * sizeof(PerformanceEntryVersion1);
+    detail_buffer = {reinterpret_cast<PerformanceDetailVersion1*>(buffer_offset), max_detail_count};
+
+    // After the current, is a ringbuffer of history frames, the current frame will be copied here
+    // before a new frame is written.
+    frame_history = std::span<u8>(workbuffer.data() + frame_size, workbuffer_size - frame_size);
+
+    // If there's room for any history frames.
+    if (frame_count >= 2) {
+        buffer_offset = frame_history.data();
+        frame_history_header = reinterpret_cast<PerformanceFrameHeaderVersion1*>(buffer_offset);
+        buffer_offset += sizeof(PerformanceFrameHeaderVersion1);
+        frame_history_entries = {reinterpret_cast<PerformanceEntryVersion1*>(buffer_offset),
+                                 entries_per_frame};
+        buffer_offset += entries_per_frame * sizeof(PerformanceEntryVersion1);
+        frame_history_details = {reinterpret_cast<PerformanceDetailVersion1*>(buffer_offset),
+                                 max_detail_count};
+    } else {
+        frame_history_header = {};
+        frame_history_entries = {};
+        frame_history_details = {};
+    }
+
+    target_node_id = 0;
+    version = PerformanceVersion(behavior.GetPerformanceMetricsDataFormat());
+    entry_count = 0;
+    detail_count = 0;
+    frame_header->entry_count = 0;
+    frame_header->detail_count = 0;
+    output_frame_index = 0;
+    last_output_frame_index = 0;
+    is_initialized = true;
+}
+
+template <>
+bool PerformanceManagerImpl<PerformanceVersion::Version1, PerformanceFrameHeaderVersion1,
+                            PerformanceEntryVersion1, PerformanceDetailVersion1>::IsInitialized()
+    const {
+    return is_initialized;
+}
+
+template <>
+u32 PerformanceManagerImpl<PerformanceVersion::Version1, PerformanceFrameHeaderVersion1,
+                           PerformanceEntryVersion1,
+                           PerformanceDetailVersion1>::CopyHistories(u8* out_buffer, u64 out_size) {
+    if (out_buffer == nullptr || out_size == 0 || !is_initialized) {
+        return 0;
+    }
+
+    // Are there any new frames waiting to be output?
+    if (last_output_frame_index == output_frame_index) {
+        return 0;
+    }
+
+    PerformanceFrameHeaderVersion1* out_header{nullptr};
+    u32 out_history_size{0};
+
+    while (last_output_frame_index != output_frame_index) {
+        PerformanceFrameHeaderVersion1* history_header{nullptr};
+        std::span<PerformanceEntryVersion1> history_entries{};
+        std::span<PerformanceDetailVersion1> history_details{};
+
+        if (max_frames > 0) {
+            auto frame_offset{&frame_history[last_output_frame_index * frame_size]};
+            history_header = reinterpret_cast<PerformanceFrameHeaderVersion1*>(frame_offset);
+            frame_offset += sizeof(PerformanceFrameHeaderVersion1);
+            history_entries = {reinterpret_cast<PerformanceEntryVersion1*>(frame_offset),
+                               history_header->entry_count};
+            frame_offset += entries_per_frame * sizeof(PerformanceFrameHeaderVersion1);
+            history_details = {reinterpret_cast<PerformanceDetailVersion1*>(frame_offset),
+                               history_header->detail_count};
+        } else {
+            // Original code does not break here, but will crash when trying to dereference the
+            // header in the next if, so let's just skip this frame and continue...
+            // Hopefully this will not happen.
+            LOG_WARNING(Service_Audio,
+                        "max_frames should not be 0! Skipping frame to avoid a crash");
+            last_output_frame_index++;
+            continue;
+        }
+
+        if (out_size < history_header->entry_count * sizeof(PerformanceEntryVersion1) +
+                           history_header->detail_count * sizeof(PerformanceDetailVersion1) +
+                           2 * sizeof(PerformanceFrameHeaderVersion1)) {
+            break;
+        }
+
+        u32 out_offset{sizeof(PerformanceFrameHeaderVersion1)};
+        auto out_entries{std::span<PerformanceEntryVersion1>(
+            reinterpret_cast<PerformanceEntryVersion1*>(out_buffer + out_offset),
+            history_header->entry_count)};
+        u32 out_entry_count{0};
+        u32 total_processing_time{0};
+        for (auto& history_entry : history_entries) {
+            if (history_entry.processed_time > 0 || history_entry.start_time > 0) {
+                out_entries[out_entry_count++] = history_entry;
+                total_processing_time += history_entry.processed_time;
+            }
+        }
+
+        out_offset += static_cast<u32>(out_entry_count * sizeof(PerformanceEntryVersion1));
+        auto out_details{std::span<PerformanceDetailVersion1>(
+            reinterpret_cast<PerformanceDetailVersion1*>(out_buffer + out_offset),
+            history_header->detail_count)};
+        u32 out_detail_count{0};
+        for (auto& history_detail : history_details) {
+            if (history_detail.processed_time > 0 || history_detail.start_time > 0) {
+                out_details[out_detail_count++] = history_detail;
+            }
+        }
+
+        out_offset += static_cast<u32>(out_detail_count * sizeof(PerformanceDetailVersion1));
+        out_header = reinterpret_cast<PerformanceFrameHeaderVersion1*>(out_buffer);
+        out_header->magic = Common::MakeMagic('P', 'E', 'R', 'F');
+        out_header->entry_count = out_entry_count;
+        out_header->detail_count = out_detail_count;
+        out_header->next_offset = out_offset;
+        out_header->total_processing_time = total_processing_time;
+        out_header->frame_index = history_header->frame_index;
+
+        out_history_size += out_offset;
+
+        out_buffer += out_offset;
+        out_size -= out_offset;
+        last_output_frame_index = (last_output_frame_index + 1) % max_frames;
+    }
+
+    // We're out of frames to output, so if there's enough left in the output buffer for another
+    // header, and we output at least 1 frame, set the next header to null.
+    if (out_size > sizeof(PerformanceFrameHeaderVersion1) && out_header != nullptr) {
+        std::memset(out_buffer, 0, sizeof(PerformanceFrameHeaderVersion1));
+    }
+
+    return out_history_size;
+}
+
+template <>
+bool PerformanceManagerImpl<PerformanceVersion::Version1, PerformanceFrameHeaderVersion1,
+                            PerformanceEntryVersion1, PerformanceDetailVersion1>::
+    GetNextEntry([[maybe_unused]] PerformanceEntryAddresses& addresses, [[maybe_unused]] u32** unk,
+                 [[maybe_unused]] PerformanceSysDetailType sys_detail_type,
+                 [[maybe_unused]] s32 node_id) {
+    return false;
+}
+
+template <>
+bool PerformanceManagerImpl<
+    PerformanceVersion::Version1, PerformanceFrameHeaderVersion1, PerformanceEntryVersion1,
+    PerformanceDetailVersion1>::GetNextEntry(PerformanceEntryAddresses& addresses,
+                                             const PerformanceEntryType entry_type,
+                                             const s32 node_id) {
+    if (!is_initialized) {
+        return false;
+    }
+
+    addresses.translated_address = translated_buffer;
+    addresses.header_entry_count_offset = CpuAddr(frame_header) - CpuAddr(workbuffer.data()) +
+                                          offsetof(PerformanceFrameHeaderVersion1, entry_count);
+
+    auto entry{&entry_buffer[entry_count++]};
+    addresses.entry_start_time_offset = CpuAddr(entry) - CpuAddr(workbuffer.data()) +
+                                        offsetof(PerformanceEntryVersion1, start_time);
+    addresses.entry_processed_time_offset = CpuAddr(entry) - CpuAddr(workbuffer.data()) +
+                                            offsetof(PerformanceEntryVersion1, processed_time);
+
+    std::memset(entry, 0, sizeof(PerformanceEntryVersion1));
+    entry->node_id = node_id;
+    entry->entry_type = entry_type;
+    return true;
+}
+
+template <>
+bool PerformanceManagerImpl<
+    PerformanceVersion::Version1, PerformanceFrameHeaderVersion1, PerformanceEntryVersion1,
+    PerformanceDetailVersion1>::GetNextEntry(PerformanceEntryAddresses& addresses,
+                                             const PerformanceDetailType detail_type,
+                                             const PerformanceEntryType entry_type,
+                                             const s32 node_id) {
+    if (!is_initialized || detail_count > MaxDetailEntries) {
+        return false;
+    }
+
+    auto detail{&detail_buffer[detail_count++]};
+
+    addresses.translated_address = translated_buffer;
+    addresses.header_entry_count_offset = CpuAddr(frame_header) - CpuAddr(workbuffer.data()) +
+                                          offsetof(PerformanceFrameHeaderVersion1, detail_count);
+    addresses.entry_start_time_offset = CpuAddr(detail) - CpuAddr(workbuffer.data()) +
+                                        offsetof(PerformanceDetailVersion1, start_time);
+    addresses.entry_processed_time_offset = CpuAddr(detail) - CpuAddr(workbuffer.data()) +
+                                            offsetof(PerformanceDetailVersion1, processed_time);
+
+    std::memset(detail, 0, sizeof(PerformanceDetailVersion1));
+    detail->node_id = node_id;
+    detail->entry_type = entry_type;
+    detail->detail_type = detail_type;
+    return true;
+}
+
+template <>
+void PerformanceManagerImpl<
+    PerformanceVersion::Version1, PerformanceFrameHeaderVersion1, PerformanceEntryVersion1,
+    PerformanceDetailVersion1>::TapFrame([[maybe_unused]] bool dsp_behind,
+                                         [[maybe_unused]] u32 voices_dropped,
+                                         [[maybe_unused]] u64 rendering_start_tick) {
+    if (!is_initialized) {
+        return;
+    }
+
+    if (max_frames > 0) {
+        if (!frame_history.empty() && !workbuffer.empty()) {
+            auto history_frame = reinterpret_cast<PerformanceFrameHeaderVersion1*>(
+                &frame_history[output_frame_index * frame_size]);
+            std::memcpy(history_frame, workbuffer.data(), frame_size);
+            history_frame->frame_index = history_frame_index++;
+        }
+        output_frame_index = (output_frame_index + 1) % max_frames;
+    }
+
+    entry_count = 0;
+    detail_count = 0;
+    frame_header->entry_count = 0;
+    frame_header->detail_count = 0;
+}
+
+template <>
+bool PerformanceManagerImpl<
+    PerformanceVersion::Version1, PerformanceFrameHeaderVersion1, PerformanceEntryVersion1,
+    PerformanceDetailVersion1>::IsDetailTarget(const u32 target_node_id_) const {
+    return target_node_id == target_node_id_;
+}
+
+template <>
+void PerformanceManagerImpl<PerformanceVersion::Version1, PerformanceFrameHeaderVersion1,
+                            PerformanceEntryVersion1,
+                            PerformanceDetailVersion1>::SetDetailTarget(const u32 target_node_id_) {
+    target_node_id = target_node_id_;
+}
+
+template <>
+void PerformanceManagerImpl<
+    PerformanceVersion::Version2, PerformanceFrameHeaderVersion2, PerformanceEntryVersion2,
+    PerformanceDetailVersion2>::Initialize(std::span<u8> workbuffer_, const u64 workbuffer_size,
+                                           const AudioRendererParameterInternal& params,
+                                           const BehaviorInfo& behavior,
+                                           const MemoryPoolInfo& memory_pool) {
+    workbuffer = workbuffer_;
+    entries_per_frame = params.voices + params.effects + params.sinks + params.sub_mixes + 1;
+    max_detail_count = MaxDetailEntries;
+    frame_size = GetRequiredBufferSizeForPerformanceMetricsPerFrame(behavior, params);
+    const auto frame_count{static_cast<u32>(workbuffer_size / frame_size)};
+    max_frames = frame_count - 1;
+    translated_buffer = memory_pool.Translate(CpuAddr(workbuffer.data()), workbuffer_size);
+
+    // The first frame is the "current" frame we're writing to.
+    auto buffer_offset{workbuffer.data()};
+    frame_header = reinterpret_cast<PerformanceFrameHeaderVersion2*>(buffer_offset);
+    buffer_offset += sizeof(PerformanceFrameHeaderVersion2);
+    entry_buffer = {reinterpret_cast<PerformanceEntryVersion2*>(buffer_offset), entries_per_frame};
+    buffer_offset += entries_per_frame * sizeof(PerformanceEntryVersion2);
+    detail_buffer = {reinterpret_cast<PerformanceDetailVersion2*>(buffer_offset), max_detail_count};
+
+    // After the current, is a ringbuffer of history frames, the current frame will be copied here
+    // before a new frame is written.
+    frame_history = std::span<u8>(workbuffer.data() + frame_size, workbuffer_size - frame_size);
+
+    // If there's room for any history frames.
+    if (frame_count >= 2) {
+        buffer_offset = frame_history.data();
+        frame_history_header = reinterpret_cast<PerformanceFrameHeaderVersion2*>(buffer_offset);
+        buffer_offset += sizeof(PerformanceFrameHeaderVersion2);
+        frame_history_entries = {reinterpret_cast<PerformanceEntryVersion2*>(buffer_offset),
+                                 entries_per_frame};
+        buffer_offset += entries_per_frame * sizeof(PerformanceEntryVersion2);
+        frame_history_details = {reinterpret_cast<PerformanceDetailVersion2*>(buffer_offset),
+                                 max_detail_count};
+    } else {
+        frame_history_header = {};
+        frame_history_entries = {};
+        frame_history_details = {};
+    }
+
+    target_node_id = 0;
+    version = PerformanceVersion(behavior.GetPerformanceMetricsDataFormat());
+    entry_count = 0;
+    detail_count = 0;
+    frame_header->entry_count = 0;
+    frame_header->detail_count = 0;
+    output_frame_index = 0;
+    last_output_frame_index = 0;
+    is_initialized = true;
+}
+
+template <>
+bool PerformanceManagerImpl<PerformanceVersion::Version2, PerformanceFrameHeaderVersion2,
+                            PerformanceEntryVersion2, PerformanceDetailVersion2>::IsInitialized()
+    const {
+    return is_initialized;
+}
+
+template <>
+u32 PerformanceManagerImpl<PerformanceVersion::Version2, PerformanceFrameHeaderVersion2,
+                           PerformanceEntryVersion2,
+                           PerformanceDetailVersion2>::CopyHistories(u8* out_buffer, u64 out_size) {
+    if (out_buffer == nullptr || out_size == 0 || !is_initialized) {
+        return 0;
+    }
+
+    // Are there any new frames waiting to be output?
+    if (last_output_frame_index == output_frame_index) {
+        return 0;
+    }
+
+    PerformanceFrameHeaderVersion2* out_header{nullptr};
+    u32 out_history_size{0};
+
+    while (last_output_frame_index != output_frame_index) {
+        PerformanceFrameHeaderVersion2* history_header{nullptr};
+        std::span<PerformanceEntryVersion2> history_entries{};
+        std::span<PerformanceDetailVersion2> history_details{};
+
+        if (max_frames > 0) {
+            auto frame_offset{&frame_history[last_output_frame_index * frame_size]};
+            history_header = reinterpret_cast<PerformanceFrameHeaderVersion2*>(frame_offset);
+            frame_offset += sizeof(PerformanceFrameHeaderVersion2);
+            history_entries = {reinterpret_cast<PerformanceEntryVersion2*>(frame_offset),
+                               history_header->entry_count};
+            frame_offset += entries_per_frame * sizeof(PerformanceFrameHeaderVersion2);
+            history_details = {reinterpret_cast<PerformanceDetailVersion2*>(frame_offset),
+                               history_header->detail_count};
+        } else {
+            // Original code does not break here, but will crash when trying to dereference the
+            // header in the next if, so let's just skip this frame and continue...
+            // Hopefully this will not happen.
+            LOG_WARNING(Service_Audio,
+                        "max_frames should not be 0! Skipping frame to avoid a crash");
+            last_output_frame_index++;
+            continue;
+        }
+
+        if (out_size < history_header->entry_count * sizeof(PerformanceEntryVersion2) +
+                           history_header->detail_count * sizeof(PerformanceDetailVersion2) +
+                           2 * sizeof(PerformanceFrameHeaderVersion2)) {
+            break;
+        }
+
+        u32 out_offset{sizeof(PerformanceFrameHeaderVersion2)};
+        auto out_entries{std::span<PerformanceEntryVersion2>(
+            reinterpret_cast<PerformanceEntryVersion2*>(out_buffer + out_offset),
+            history_header->entry_count)};
+        u32 out_entry_count{0};
+        u32 total_processing_time{0};
+        for (auto& history_entry : history_entries) {
+            if (history_entry.processed_time > 0 || history_entry.start_time > 0) {
+                out_entries[out_entry_count++] = history_entry;
+                total_processing_time += history_entry.processed_time;
+            }
+        }
+
+        out_offset += static_cast<u32>(out_entry_count * sizeof(PerformanceEntryVersion2));
+        auto out_details{std::span<PerformanceDetailVersion2>(
+            reinterpret_cast<PerformanceDetailVersion2*>(out_buffer + out_offset),
+            history_header->detail_count)};
+        u32 out_detail_count{0};
+        for (auto& history_detail : history_details) {
+            if (history_detail.processed_time > 0 || history_detail.start_time > 0) {
+                out_details[out_detail_count++] = history_detail;
+            }
+        }
+
+        out_offset += static_cast<u32>(out_detail_count * sizeof(PerformanceDetailVersion2));
+        out_header = reinterpret_cast<PerformanceFrameHeaderVersion2*>(out_buffer);
+        out_header->magic = Common::MakeMagic('P', 'E', 'R', 'F');
+        out_header->entry_count = out_entry_count;
+        out_header->detail_count = out_detail_count;
+        out_header->next_offset = out_offset;
+        out_header->total_processing_time = total_processing_time;
+        out_header->voices_dropped = history_header->voices_dropped;
+        out_header->start_time = history_header->start_time;
+        out_header->frame_index = history_header->frame_index;
+        out_header->render_time_exceeded = history_header->render_time_exceeded;
+
+        out_history_size += out_offset;
+
+        out_buffer += out_offset;
+        out_size -= out_offset;
+        last_output_frame_index = (last_output_frame_index + 1) % max_frames;
+    }
+
+    // We're out of frames to output, so if there's enough left in the output buffer for another
+    // header, and we output at least 1 frame, set the next header to null.
+    if (out_size > sizeof(PerformanceFrameHeaderVersion2) && out_header != nullptr) {
+        std::memset(out_buffer, 0, sizeof(PerformanceFrameHeaderVersion2));
+    }
+
+    return out_history_size;
+}
+
+template <>
+bool PerformanceManagerImpl<
+    PerformanceVersion::Version2, PerformanceFrameHeaderVersion2, PerformanceEntryVersion2,
+    PerformanceDetailVersion2>::GetNextEntry(PerformanceEntryAddresses& addresses, u32** unk,
+                                             const PerformanceSysDetailType sys_detail_type,
+                                             const s32 node_id) {
+    if (!is_initialized || detail_count > MaxDetailEntries) {
+        return false;
+    }
+
+    auto detail{&detail_buffer[detail_count++]};
+
+    addresses.translated_address = translated_buffer;
+    addresses.header_entry_count_offset = CpuAddr(frame_header) - CpuAddr(workbuffer.data()) +
+                                          offsetof(PerformanceFrameHeaderVersion2, detail_count);
+    addresses.entry_start_time_offset = CpuAddr(detail) - CpuAddr(workbuffer.data()) +
+                                        offsetof(PerformanceDetailVersion2, start_time);
+    addresses.entry_processed_time_offset = CpuAddr(detail) - CpuAddr(workbuffer.data()) +
+                                            offsetof(PerformanceDetailVersion2, processed_time);
+
+    std::memset(detail, 0, sizeof(PerformanceDetailVersion2));
+    detail->node_id = node_id;
+    detail->detail_type = static_cast<PerformanceDetailType>(sys_detail_type);
+
+    if (unk) {
+        *unk = &detail->unk_10;
+    }
+    return true;
+}
+
+template <>
+bool PerformanceManagerImpl<
+    PerformanceVersion::Version2, PerformanceFrameHeaderVersion2, PerformanceEntryVersion2,
+    PerformanceDetailVersion2>::GetNextEntry(PerformanceEntryAddresses& addresses,
+                                             const PerformanceEntryType entry_type,
+                                             const s32 node_id) {
+    if (!is_initialized) {
+        return false;
+    }
+
+    auto entry{&entry_buffer[entry_count++]};
+
+    addresses.translated_address = translated_buffer;
+    addresses.header_entry_count_offset = CpuAddr(frame_header) - CpuAddr(workbuffer.data()) +
+                                          offsetof(PerformanceFrameHeaderVersion2, entry_count);
+    addresses.entry_start_time_offset = CpuAddr(entry) - CpuAddr(workbuffer.data()) +
+                                        offsetof(PerformanceEntryVersion2, start_time);
+    addresses.entry_processed_time_offset = CpuAddr(entry) - CpuAddr(workbuffer.data()) +
+                                            offsetof(PerformanceEntryVersion2, processed_time);
+
+    std::memset(entry, 0, sizeof(PerformanceEntryVersion2));
+    entry->node_id = node_id;
+    entry->entry_type = entry_type;
+    return true;
+}
+
+template <>
+bool PerformanceManagerImpl<
+    PerformanceVersion::Version2, PerformanceFrameHeaderVersion2, PerformanceEntryVersion2,
+    PerformanceDetailVersion2>::GetNextEntry(PerformanceEntryAddresses& addresses,
+                                             const PerformanceDetailType detail_type,
+                                             const PerformanceEntryType entry_type,
+                                             const s32 node_id) {
+    if (!is_initialized || detail_count > MaxDetailEntries) {
+        return false;
+    }
+
+    auto detail{&detail_buffer[detail_count++]};
+
+    addresses.translated_address = translated_buffer;
+    addresses.header_entry_count_offset = CpuAddr(frame_header) - CpuAddr(workbuffer.data()) +
+                                          offsetof(PerformanceFrameHeaderVersion2, detail_count);
+    addresses.entry_start_time_offset = CpuAddr(detail) - CpuAddr(workbuffer.data()) +
+                                        offsetof(PerformanceDetailVersion2, start_time);
+    addresses.entry_processed_time_offset = CpuAddr(detail) - CpuAddr(workbuffer.data()) +
+                                            offsetof(PerformanceDetailVersion2, processed_time);
+
+    std::memset(detail, 0, sizeof(PerformanceDetailVersion2));
+    detail->node_id = node_id;
+    detail->entry_type = entry_type;
+    detail->detail_type = detail_type;
+    return true;
+}
+
+template <>
+void PerformanceManagerImpl<PerformanceVersion::Version2, PerformanceFrameHeaderVersion2,
+                            PerformanceEntryVersion2,
+                            PerformanceDetailVersion2>::TapFrame(const bool dsp_behind,
+                                                                 const u32 voices_dropped,
+                                                                 const u64 rendering_start_tick) {
+    if (!is_initialized) {
+        return;
+    }
+
+    if (max_frames > 0) {
+        if (!frame_history.empty() && !workbuffer.empty()) {
+            auto history_frame{reinterpret_cast<PerformanceFrameHeaderVersion2*>(
+                &frame_history[output_frame_index * frame_size])};
+            std::memcpy(history_frame, workbuffer.data(), frame_size);
+            history_frame->render_time_exceeded = dsp_behind;
+            history_frame->voices_dropped = voices_dropped;
+            history_frame->start_time = rendering_start_tick;
+            history_frame->frame_index = history_frame_index++;
+        }
+        output_frame_index = (output_frame_index + 1) % max_frames;
+    }
+
+    entry_count = 0;
+    detail_count = 0;
+    frame_header->entry_count = 0;
+    frame_header->detail_count = 0;
+}
+
+template <>
+bool PerformanceManagerImpl<
+    PerformanceVersion::Version2, PerformanceFrameHeaderVersion2, PerformanceEntryVersion2,
+    PerformanceDetailVersion2>::IsDetailTarget(const u32 target_node_id_) const {
+    return target_node_id == target_node_id_;
+}
+
+template <>
+void PerformanceManagerImpl<PerformanceVersion::Version2, PerformanceFrameHeaderVersion2,
+                            PerformanceEntryVersion2,
+                            PerformanceDetailVersion2>::SetDetailTarget(const u32 target_node_id_) {
+    target_node_id = target_node_id_;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/performance/performance_manager.h b/src/audio_core/renderer/performance/performance_manager.h
new file mode 100644
index 0000000000..b82176bef1
--- /dev/null
+++ b/src/audio_core/renderer/performance/performance_manager.h
@@ -0,0 +1,273 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <chrono>
+#include <memory>
+#include <span>
+
+#include "audio_core/common/audio_renderer_parameter.h"
+#include "audio_core/renderer/performance/performance_detail.h"
+#include "audio_core/renderer/performance/performance_entry.h"
+#include "audio_core/renderer/performance/performance_entry_addresses.h"
+#include "audio_core/renderer/performance/performance_frame_header.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+class BehaviorInfo;
+class MemoryPoolInfo;
+
+enum class PerformanceVersion {
+    Version1,
+    Version2,
+};
+
+enum class PerformanceSysDetailType {
+    PcmInt16 = 15,
+    PcmFloat = 16,
+    Adpcm = 17,
+    LightLimiter = 37,
+};
+
+enum class PerformanceState {
+    Invalid,
+    Start,
+    Stop,
+};
+
+/**
+ * Manages performance information.
+ *
+ * The performance buffer is split into frames, each comprised of:
+ *     Frame header - Information about the number of entries/details and some others
+ *     Entries      - Created when starting to generate types of commands, such as voice
+ * commands, mix commands, sink commands etc. Details      - Created for specific commands
+ * within each group. Up to MaxDetailEntries per frame.
+ *
+ * A current frame is written to by the AudioRenderer, and before it processes the next command
+ * list, the current frame is copied to a ringbuffer of history frames. These frames are then
+ * output back to the game if it supplies a performance buffer to RequestUpdate.
+ *
+ * Two versions currently exist, version 2 adds a few extra fields to the header, and a new
+ * SysDetail type which is seemingly unused.
+ */
+class PerformanceManager {
+public:
+    static constexpr size_t MaxDetailEntries = 100;
+
+    struct InParameter {
+        /* 0x00 */ s32 target_node_id;
+        /* 0x04 */ char unk04[0xC];
+    };
+    static_assert(sizeof(InParameter) == 0x10,
+                  "PerformanceManager::InParameter has the wrong size!");
+
+    struct OutStatus {
+        /* 0x00 */ s32 history_size;
+        /* 0x04 */ char unk04[0xC];
+    };
+    static_assert(sizeof(OutStatus) == 0x10, "PerformanceManager::OutStatus has the wrong size!");
+
+    /**
+     * Calculate the required size for the performance workbuffer.
+     *
+     * @param behavior - Check which version is supported.
+     * @param params    - Input parameters.
+     * @return Required workbuffer size.
+     */
+    static u64 GetRequiredBufferSizeForPerformanceMetricsPerFrame(
+        const BehaviorInfo& behavior, const AudioRendererParameterInternal& params) {
+        u64 entry_count{params.voices + params.effects + params.sub_mixes + params.sinks + 1};
+        switch (behavior.GetPerformanceMetricsDataFormat()) {
+        case 1:
+            return sizeof(PerformanceFrameHeaderVersion1) +
+                   PerformanceManager::MaxDetailEntries * sizeof(PerformanceDetailVersion1) +
+                   entry_count * sizeof(PerformanceEntryVersion1);
+        case 2:
+            return sizeof(PerformanceFrameHeaderVersion2) +
+                   PerformanceManager::MaxDetailEntries * sizeof(PerformanceDetailVersion2) +
+                   entry_count * sizeof(PerformanceEntryVersion2);
+        }
+
+        LOG_WARNING(Service_Audio, "Invalid PerformanceMetrics version, assuming version 1");
+        return sizeof(PerformanceFrameHeaderVersion1) +
+               PerformanceManager::MaxDetailEntries * sizeof(PerformanceDetailVersion1) +
+               entry_count * sizeof(PerformanceEntryVersion1);
+    }
+
+    virtual ~PerformanceManager() = default;
+
+    /**
+     * Initialize the performance manager.
+     *
+     * @param workbuffer      - Workbuffer to use for performance frames.
+     * @param workbuffer_size - Size of the workbuffer.
+     * @param params          - Input parameters.
+     * @param behavior       - Behaviour to check version and data format.
+     * @param memory_pool     - Used to translate the workbuffer address for the DSP.
+     */
+    virtual void Initialize(std::span<u8> workbuffer, u64 workbuffer_size,
+                            const AudioRendererParameterInternal& params,
+                            const BehaviorInfo& behavior, const MemoryPoolInfo& memory_pool);
+
+    /**
+     * Check if the manager is initialized.
+     *
+     * @return True if initialized, otherwise false.
+     */
+    virtual bool IsInitialized() const;
+
+    /**
+     * Copy the waiting performance frames to the output buffer.
+     *
+     * @param out_buffer - Output buffer to store performance frames.
+     * @param out_size   - Size of the output buffer.
+     * @return Size in bytes that were written to the buffer.
+     */
+    virtual u32 CopyHistories(u8* out_buffer, u64 out_size);
+
+    /**
+     * Setup a new sys detail in the current frame, filling in addresses with offsets to the
+     * current workbuffer, to be written by the AudioRenderer. Note: This version is
+     * unused/incomplete.
+     *
+     * @param addresses       - Filled with pointers to the new entry, which should be passed to
+     * the AudioRenderer with Performance commands to be written.
+     * @param unk             - Unknown.
+     * @param sys_detail_type - Sys detail type.
+     * @param node_id         - Node id for this entry.
+     * @return True if a new entry was created and the offsets are valid, otherwise false.
+     */
+    virtual bool GetNextEntry(PerformanceEntryAddresses& addresses, u32** unk,
+                              PerformanceSysDetailType sys_detail_type, s32 node_id);
+
+    /**
+     * Setup a new entry in the current frame, filling in addresses with offsets to the current
+     * workbuffer, to be written by the AudioRenderer.
+     *
+     * @param addresses       - Filled with pointers to the new entry, which should be passed to
+     * the AudioRenderer with Performance commands to be written.
+     * @param entry_type      - The type of this entry. See PerformanceEntryType
+     * @param node_id         - Node id for this entry.
+     * @return True if a new entry was created and the offsets are valid, otherwise false.
+     */
+    virtual bool GetNextEntry(PerformanceEntryAddresses& addresses, PerformanceEntryType entry_type,
+                              s32 node_id);
+
+    /**
+     * Setup a new detail in the current frame, filling in addresses with offsets to the current
+     * workbuffer, to be written by the AudioRenderer.
+     *
+     * @param addresses       - Filled with pointers to the new detail, which should be passed
+     * to the AudioRenderer with Performance commands to be written.
+     * @param entry_type      - The type of this detail. See PerformanceEntryType
+     * @param node_id         - Node id for this detail.
+     * @return True if a new detail was created and the offsets are valid, otherwise false.
+     */
+    virtual bool GetNextEntry(PerformanceEntryAddresses& addresses,
+                              PerformanceDetailType detail_type, PerformanceEntryType entry_type,
+                              s32 node_id);
+
+    /**
+     * Save the current frame to the ring buffer.
+     *
+     * @param dsp_behind           - Did the AudioRenderer fall behind and not
+     *                               finish processing the command list?
+     * @param voices_dropped       - The number of voices that were dropped.
+     * @param rendering_start_tick - The tick rendering started.
+     */
+    virtual void TapFrame(bool dsp_behind, u32 voices_dropped, u64 rendering_start_tick);
+
+    /**
+     * Check if the node id is a detail type.
+     *
+     * @return True if the node is a detail type, otherwise false.
+     */
+    virtual bool IsDetailTarget(u32 target_node_id) const;
+
+    /**
+     * Set the given node to be a detail type.
+     *
+     * @param target_node_id - Node to set.
+     */
+    virtual void SetDetailTarget(u32 target_node_id);
+
+private:
+    /**
+     * Create the performance manager.
+     *
+     * @param version - Performance version to create.
+     */
+    void CreateImpl(size_t version);
+
+    std::unique_ptr<PerformanceManager>
+        /// Impl for the performance manager, may be version 1 or 2.
+        impl;
+};
+
+template <PerformanceVersion Version, typename FrameHeaderVersion, typename EntryVersion,
+          typename DetailVersion>
+class PerformanceManagerImpl : public PerformanceManager {
+public:
+    void Initialize(std::span<u8> workbuffer, u64 workbuffer_size,
+                    const AudioRendererParameterInternal& params, const BehaviorInfo& behavior,
+                    const MemoryPoolInfo& memory_pool) override;
+    bool IsInitialized() const override;
+    u32 CopyHistories(u8* out_buffer, u64 out_size) override;
+    bool GetNextEntry(PerformanceEntryAddresses& addresses, u32** unk,
+                      PerformanceSysDetailType sys_detail_type, s32 node_id) override;
+    bool GetNextEntry(PerformanceEntryAddresses& addresses, PerformanceEntryType entry_type,
+                      s32 node_id) override;
+    bool GetNextEntry(PerformanceEntryAddresses& addresses, PerformanceDetailType detail_type,
+                      PerformanceEntryType entry_type, s32 node_id) override;
+    void TapFrame(bool dsp_behind, u32 voices_dropped, u64 rendering_start_tick) override;
+    bool IsDetailTarget(u32 target_node_id) const override;
+    void SetDetailTarget(u32 target_node_id) override;
+
+private:
+    /// Workbuffer used to store the current performance frame
+    std::span<u8> workbuffer{};
+    /// DSP address of the workbuffer, used by the AudioRenderer
+    CpuAddr translated_buffer{};
+    /// Current frame index
+    u32 history_frame_index{};
+    /// Current frame header
+    FrameHeaderVersion* frame_header{};
+    /// Current frame entry buffer
+    std::span<EntryVersion> entry_buffer{};
+    /// Current frame detail buffer
+    std::span<DetailVersion> detail_buffer{};
+    /// Current frame entry count
+    u32 entry_count{};
+    /// Current frame detail count
+    u32 detail_count{};
+    /// Ringbuffer of previous frames
+    std::span<u8> frame_history{};
+    /// Current history frame header
+    FrameHeaderVersion* frame_history_header{};
+    /// Current history entry buffer
+    std::span<EntryVersion> frame_history_entries{};
+    /// Current history detail buffer
+    std::span<DetailVersion> frame_history_details{};
+    /// Current history ringbuffer write index
+    u32 output_frame_index{};
+    /// Last history frame index that was written back to the game
+    u32 last_output_frame_index{};
+    /// Maximum number of history frames in the ringbuffer
+    u32 max_frames{};
+    /// Number of entries per frame
+    u32 entries_per_frame{};
+    /// Maximum number of details per frame
+    u32 max_detail_count{};
+    /// Frame size in bytes
+    u64 frame_size{};
+    /// Is the performance manager initialized?
+    bool is_initialized{};
+    /// Target node id
+    u32 target_node_id{};
+    /// Performance version in use
+    PerformanceVersion version{};
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/sink/circular_buffer_sink_info.cpp b/src/audio_core/renderer/sink/circular_buffer_sink_info.cpp
new file mode 100644
index 0000000000..d91f104025
--- /dev/null
+++ b/src/audio_core/renderer/sink/circular_buffer_sink_info.cpp
@@ -0,0 +1,76 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/memory/pool_mapper.h"
+#include "audio_core/renderer/sink/circular_buffer_sink_info.h"
+#include "audio_core/renderer/upsampler/upsampler_manager.h"
+
+namespace AudioCore::AudioRenderer {
+
+CircularBufferSinkInfo::CircularBufferSinkInfo() {
+    state.fill(0);
+    parameter.fill(0);
+    type = Type::CircularBufferSink;
+
+    auto state_{reinterpret_cast<CircularBufferState*>(state.data())};
+    state_->address_info.Setup(0, 0);
+}
+
+void CircularBufferSinkInfo::CleanUp() {
+    auto state_{reinterpret_cast<DeviceState*>(state.data())};
+
+    if (state_->upsampler_info) {
+        state_->upsampler_info->manager->Free(state_->upsampler_info);
+        state_->upsampler_info = nullptr;
+    }
+
+    parameter.fill(0);
+    type = Type::Invalid;
+}
+
+void CircularBufferSinkInfo::Update(BehaviorInfo::ErrorInfo& error_info, OutStatus& out_status,
+                                    const InParameter& in_params, const PoolMapper& pool_mapper) {
+    const auto buffer_params{
+        reinterpret_cast<const CircularBufferInParameter*>(&in_params.circular_buffer)};
+    auto current_params{reinterpret_cast<CircularBufferInParameter*>(parameter.data())};
+    auto current_state{reinterpret_cast<CircularBufferState*>(state.data())};
+
+    if (in_use == buffer_params->in_use && !buffer_unmapped) {
+        error_info.error_code = ResultSuccess;
+        error_info.address = CpuAddr(0);
+        out_status.writeOffset = current_state->last_pos2;
+        return;
+    }
+
+    node_id = in_params.node_id;
+    in_use = in_params.in_use;
+
+    if (in_use) {
+        buffer_unmapped =
+            !pool_mapper.TryAttachBuffer(error_info, current_state->address_info,
+                                         buffer_params->cpu_address, buffer_params->size);
+        *current_params = *buffer_params;
+    } else {
+        *current_params = *buffer_params;
+    }
+    out_status.writeOffset = current_state->last_pos2;
+}
+
+void CircularBufferSinkInfo::UpdateForCommandGeneration() {
+    if (in_use) {
+        auto params{reinterpret_cast<CircularBufferInParameter*>(parameter.data())};
+        auto state_{reinterpret_cast<CircularBufferState*>(state.data())};
+
+        const auto pos{state_->current_pos};
+        state_->last_pos2 = state_->last_pos;
+        state_->last_pos = pos;
+
+        state_->current_pos += static_cast<s32>(params->input_count * params->sample_count *
+                                                GetSampleFormatByteSize(SampleFormat::PcmInt16));
+        if (params->size > 0) {
+            state_->current_pos %= params->size;
+        }
+    }
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/sink/circular_buffer_sink_info.h b/src/audio_core/renderer/sink/circular_buffer_sink_info.h
new file mode 100644
index 0000000000..3356213ea7
--- /dev/null
+++ b/src/audio_core/renderer/sink/circular_buffer_sink_info.h
@@ -0,0 +1,41 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "audio_core/renderer/sink/sink_info_base.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * Info for a circular buffer sink.
+ */
+class CircularBufferSinkInfo : public SinkInfoBase {
+public:
+    CircularBufferSinkInfo();
+
+    /**
+     * Clean up for info, resetting it to a default state.
+     */
+    void CleanUp() override;
+
+    /**
+     * Update the info according to parameters, and write the current state to out_status.
+     *
+     * @param error_info  - Output error code.
+     * @param out_status  - Output status.
+     * @param in_params   - Input parameters.
+     * @param pool_mapper - Used to map the circular buffer.
+     */
+    void Update(BehaviorInfo::ErrorInfo& error_info, OutStatus& out_status,
+                const InParameter& in_params, const PoolMapper& pool_mapper) override;
+
+    /**
+     * Update the circular buffer on command generation, incrementing its current offsets.
+     */
+    void UpdateForCommandGeneration() override;
+};
+static_assert(sizeof(CircularBufferSinkInfo) <= sizeof(SinkInfoBase),
+              "CircularBufferSinkInfo is too large!");
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/sink/device_sink_info.cpp b/src/audio_core/renderer/sink/device_sink_info.cpp
new file mode 100644
index 0000000000..b7b3d6f1db
--- /dev/null
+++ b/src/audio_core/renderer/sink/device_sink_info.cpp
@@ -0,0 +1,57 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/sink/device_sink_info.h"
+#include "audio_core/renderer/upsampler/upsampler_manager.h"
+
+namespace AudioCore::AudioRenderer {
+
+DeviceSinkInfo::DeviceSinkInfo() {
+    state.fill(0);
+    parameter.fill(0);
+    type = Type::DeviceSink;
+}
+
+void DeviceSinkInfo::CleanUp() {
+    auto state_{reinterpret_cast<DeviceState*>(state.data())};
+
+    if (state_->upsampler_info) {
+        state_->upsampler_info->manager->Free(state_->upsampler_info);
+        state_->upsampler_info = nullptr;
+    }
+
+    parameter.fill(0);
+    type = Type::Invalid;
+}
+
+void DeviceSinkInfo::Update(BehaviorInfo::ErrorInfo& error_info, OutStatus& out_status,
+                            const InParameter& in_params,
+                            [[maybe_unused]] const PoolMapper& pool_mapper) {
+
+    const auto device_params{reinterpret_cast<const DeviceInParameter*>(&in_params.device)};
+    auto current_params{reinterpret_cast<DeviceInParameter*>(parameter.data())};
+
+    if (in_use == in_params.in_use) {
+        current_params->downmix_enabled = device_params->downmix_enabled;
+        current_params->downmix_coeff = device_params->downmix_coeff;
+    } else {
+        type = in_params.type;
+        in_use = in_params.in_use;
+        node_id = in_params.node_id;
+        *current_params = *device_params;
+    }
+
+    auto current_state{reinterpret_cast<DeviceState*>(state.data())};
+
+    for (size_t i = 0; i < current_state->downmix_coeff.size(); i++) {
+        current_state->downmix_coeff[i] = current_params->downmix_coeff[i];
+    }
+
+    std::memset(&out_status, 0, sizeof(OutStatus));
+    error_info.error_code = ResultSuccess;
+    error_info.address = CpuAddr(0);
+}
+
+void DeviceSinkInfo::UpdateForCommandGeneration() {}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/sink/device_sink_info.h b/src/audio_core/renderer/sink/device_sink_info.h
new file mode 100644
index 0000000000..a1c4414547
--- /dev/null
+++ b/src/audio_core/renderer/sink/device_sink_info.h
@@ -0,0 +1,40 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "audio_core/renderer/sink/sink_info_base.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * Info for a device sink.
+ */
+class DeviceSinkInfo : public SinkInfoBase {
+public:
+    DeviceSinkInfo();
+
+    /**
+     * Clean up for info, resetting it to a default state.
+     */
+    void CleanUp() override;
+
+    /**
+     * Update the info according to parameters, and write the current state to out_status.
+     *
+     * @param error_info  - Output error code.
+     * @param out_status  - Output status.
+     * @param in_params   - Input parameters.
+     * @param pool_mapper - Unused.
+     */
+    void Update(BehaviorInfo::ErrorInfo& error_info, OutStatus& out_status,
+                const InParameter& in_params, const PoolMapper& pool_mapper) override;
+
+    /**
+     * Update the device sink on command generation, unused.
+     */
+    void UpdateForCommandGeneration() override;
+};
+static_assert(sizeof(DeviceSinkInfo) <= sizeof(SinkInfoBase), "DeviceSinkInfo is too large!");
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/sink/sink_context.cpp b/src/audio_core/renderer/sink/sink_context.cpp
new file mode 100644
index 0000000000..634bc1cf95
--- /dev/null
+++ b/src/audio_core/renderer/sink/sink_context.cpp
@@ -0,0 +1,21 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/sink/sink_context.h"
+
+namespace AudioCore::AudioRenderer {
+
+void SinkContext::Initialize(std::span<SinkInfoBase> sink_infos_, const u32 sink_count_) {
+    sink_infos = sink_infos_;
+    sink_count = sink_count_;
+}
+
+SinkInfoBase* SinkContext::GetInfo(const u32 index) {
+    return &sink_infos[index];
+}
+
+u32 SinkContext::GetCount() const {
+    return sink_count;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/sink/sink_context.h b/src/audio_core/renderer/sink/sink_context.h
new file mode 100644
index 0000000000..185572e290
--- /dev/null
+++ b/src/audio_core/renderer/sink/sink_context.h
@@ -0,0 +1,47 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <span>
+
+#include "audio_core/renderer/sink/sink_info_base.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * Manages output sinks.
+ */
+class SinkContext {
+public:
+    /**
+     * Initialize the sink context.
+     *
+     * @param sink_infos - Workbuffer for the sinks.
+     * @param sink_count - Number of sinks in the buffer.
+     */
+    void Initialize(std::span<SinkInfoBase> sink_infos, u32 sink_count);
+
+    /**
+     * Get a given index's info.
+     *
+     * @param index - Sink index to get.
+     * @return The sink info base for the given index.
+     */
+    SinkInfoBase* GetInfo(u32 index);
+
+    /**
+     * Get the current number of sinks.
+     *
+     * @return The number of sinks.
+     */
+    u32 GetCount() const;
+
+private:
+    /// Buffer of sink infos
+    std::span<SinkInfoBase> sink_infos{};
+    /// Number of sinks in the buffer
+    u32 sink_count{};
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/sink/sink_info_base.cpp b/src/audio_core/renderer/sink/sink_info_base.cpp
new file mode 100644
index 0000000000..4279beaa0e
--- /dev/null
+++ b/src/audio_core/renderer/sink/sink_info_base.cpp
@@ -0,0 +1,51 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/memory/pool_mapper.h"
+#include "audio_core/renderer/sink/sink_info_base.h"
+
+namespace AudioCore::AudioRenderer {
+
+void SinkInfoBase::CleanUp() {
+    type = Type::Invalid;
+}
+
+void SinkInfoBase::Update(BehaviorInfo::ErrorInfo& error_info, OutStatus& out_status,
+                          [[maybe_unused]] const InParameter& in_params,
+                          [[maybe_unused]] const PoolMapper& pool_mapper) {
+    std::memset(&out_status, 0, sizeof(OutStatus));
+    error_info.error_code = ResultSuccess;
+    error_info.address = CpuAddr(0);
+}
+
+void SinkInfoBase::UpdateForCommandGeneration() {}
+
+SinkInfoBase::DeviceState* SinkInfoBase::GetDeviceState() {
+    return reinterpret_cast<DeviceState*>(state.data());
+}
+
+SinkInfoBase::Type SinkInfoBase::GetType() const {
+    return type;
+}
+
+bool SinkInfoBase::IsUsed() const {
+    return in_use;
+}
+
+bool SinkInfoBase::ShouldSkip() const {
+    return buffer_unmapped;
+}
+
+u32 SinkInfoBase::GetNodeId() const {
+    return node_id;
+}
+
+u8* SinkInfoBase::GetState() {
+    return state.data();
+}
+
+u8* SinkInfoBase::GetParameter() {
+    return parameter.data();
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/sink/sink_info_base.h b/src/audio_core/renderer/sink/sink_info_base.h
new file mode 100644
index 0000000000..a1b855f205
--- /dev/null
+++ b/src/audio_core/renderer/sink/sink_info_base.h
@@ -0,0 +1,177 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+
+#include "audio_core/common/common.h"
+#include "audio_core/renderer/behavior/behavior_info.h"
+#include "audio_core/renderer/memory/address_info.h"
+#include "common/common_types.h"
+#include "common/fixed_point.h"
+
+namespace AudioCore::AudioRenderer {
+struct UpsamplerInfo;
+class PoolMapper;
+
+/**
+ * Base for the circular buffer and device sinks, holding their states for the AudioRenderer and
+ * their parametetrs for generating sink commands.
+ */
+class SinkInfoBase {
+public:
+    enum class Type : u8 {
+        Invalid,
+        DeviceSink,
+        CircularBufferSink,
+    };
+
+    struct DeviceInParameter {
+        /* 0x000 */ char name[0x100];
+        /* 0x100 */ u32 input_count;
+        /* 0x104 */ std::array<s8, MaxChannels> inputs;
+        /* 0x10A */ char unk10A[0x1];
+        /* 0x10B */ bool downmix_enabled;
+        /* 0x10C */ std::array<f32, 4> downmix_coeff;
+    };
+    static_assert(sizeof(DeviceInParameter) == 0x11C, "DeviceInParameter has the wrong size!");
+
+    struct DeviceState {
+        /* 0x00 */ UpsamplerInfo* upsampler_info;
+        /* 0x08 */ std::array<Common::FixedPoint<16, 16>, 4> downmix_coeff;
+        /* 0x18 */ char unk18[0x18];
+    };
+    static_assert(sizeof(DeviceState) == 0x30, "DeviceState has the wrong size!");
+
+    struct CircularBufferInParameter {
+        /* 0x00 */ u64 cpu_address;
+        /* 0x08 */ u32 size;
+        /* 0x0C */ u32 input_count;
+        /* 0x10 */ u32 sample_count;
+        /* 0x14 */ u32 previous_pos;
+        /* 0x18 */ SampleFormat format;
+        /* 0x1C */ std::array<s8, MaxChannels> inputs;
+        /* 0x22 */ bool in_use;
+        /* 0x23 */ char unk23[0x5];
+    };
+    static_assert(sizeof(CircularBufferInParameter) == 0x28,
+                  "CircularBufferInParameter has the wrong size!");
+
+    struct CircularBufferState {
+        /* 0x00 */ u32 last_pos2;
+        /* 0x04 */ s32 current_pos;
+        /* 0x08 */ u32 last_pos;
+        /* 0x0C */ char unk0C[0x4];
+        /* 0x10 */ AddressInfo address_info;
+    };
+    static_assert(sizeof(CircularBufferState) == 0x30, "CircularBufferState has the wrong size!");
+
+    struct InParameter {
+        /* 0x000 */ Type type;
+        /* 0x001 */ bool in_use;
+        /* 0x004 */ u32 node_id;
+        /* 0x008 */ char unk08[0x18];
+        union {
+            /* 0x020 */ DeviceInParameter device;
+            /* 0x020 */ CircularBufferInParameter circular_buffer;
+        };
+    };
+    static_assert(sizeof(InParameter) == 0x140, "SinkInfoBase::InParameter has the wrong size!");
+
+    struct OutStatus {
+        /* 0x00 */ u32 writeOffset;
+        /* 0x04 */ char unk04[0x1C];
+    }; // size == 0x20
+    static_assert(sizeof(OutStatus) == 0x20, "SinkInfoBase::OutStatus has the wrong size!");
+
+    virtual ~SinkInfoBase() = default;
+
+    /**
+     * Clean up for info, resetting it to a default state.
+     */
+    virtual void CleanUp();
+
+    /**
+     * Update the info according to parameters, and write the current state to out_status.
+     *
+     * @param error_info  - Output error code.
+     * @param out_status  - Output status.
+     * @param in_params   - Input parameters.
+     * @param pool_mapper - Used to map the circular buffer.
+     */
+    virtual void Update(BehaviorInfo::ErrorInfo& error_info, OutStatus& out_status,
+                        [[maybe_unused]] const InParameter& in_params,
+                        [[maybe_unused]] const PoolMapper& pool_mapper);
+
+    /**
+     * Update the circular buffer on command generation, incrementing its current offsets.
+     */
+    virtual void UpdateForCommandGeneration();
+
+    /**
+     * Get the state as a device sink.
+     *
+     * @return Device state.
+     */
+    DeviceState* GetDeviceState();
+
+    /**
+     * Get the type of this sink.
+     *
+     * @return Either Device, Circular, or Invalid.
+     */
+    Type GetType() const;
+
+    /**
+     * Check if this sink is in use.
+     *
+     * @return True if used, otherwise false.
+     */
+    bool IsUsed() const;
+
+    /**
+     * Check if this sink should be skipped for updates.
+     *
+     * @return True if it should be skipped, otherwise false.
+     */
+    bool ShouldSkip() const;
+
+    /**
+     * Get the node if of this sink.
+     *
+     * @return Node id for this sink.
+     */
+    u32 GetNodeId() const;
+
+    /**
+     * Get the state of this sink.
+     *
+     * @return Pointer to the state, must be cast to the correct type.
+     */
+    u8* GetState();
+
+    /**
+     * Get the parameters of this sink.
+     *
+     * @return Pointer to the parameters, must be cast to the correct type.
+     */
+    u8* GetParameter();
+
+protected:
+    /// Type of this sink
+    Type type{Type::Invalid};
+    /// Is this sink in use?
+    bool in_use{};
+    /// Is this sink's buffer unmapped? Circular only
+    bool buffer_unmapped{};
+    /// Node id for this sink
+    u32 node_id{};
+    /// State buffer for this sink
+    std::array<u8, std::max(sizeof(DeviceState), sizeof(CircularBufferState))> state{};
+    /// Parameter buffer for this sink
+    std::array<u8, std::max(sizeof(DeviceInParameter), sizeof(CircularBufferInParameter))>
+        parameter{};
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/splitter/splitter_context.cpp b/src/audio_core/renderer/splitter/splitter_context.cpp
new file mode 100644
index 0000000000..7a23ba43f7
--- /dev/null
+++ b/src/audio_core/renderer/splitter/splitter_context.cpp
@@ -0,0 +1,217 @@
+// 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/common/workbuffer_allocator.h"
+#include "audio_core/renderer/behavior/behavior_info.h"
+#include "audio_core/renderer/splitter/splitter_context.h"
+#include "common/alignment.h"
+
+namespace AudioCore::AudioRenderer {
+
+SplitterDestinationData* SplitterContext::GetDesintationData(const s32 splitter_id,
+                                                             const s32 destination_id) {
+    return splitter_infos[splitter_id].GetData(destination_id);
+}
+
+SplitterInfo& SplitterContext::GetInfo(const s32 splitter_id) {
+    return splitter_infos[splitter_id];
+}
+
+u32 SplitterContext::GetDataCount() const {
+    return destinations_count;
+}
+
+u32 SplitterContext::GetInfoCount() const {
+    return info_count;
+}
+
+SplitterDestinationData& SplitterContext::GetData(const u32 index) {
+    return splitter_destinations[index];
+}
+
+void SplitterContext::Setup(std::span<SplitterInfo> splitter_infos_, const u32 splitter_info_count_,
+                            SplitterDestinationData* splitter_destinations_,
+                            const u32 destination_count_, const bool splitter_bug_fixed_) {
+    splitter_infos = splitter_infos_;
+    info_count = splitter_info_count_;
+    splitter_destinations = splitter_destinations_;
+    destinations_count = destination_count_;
+    splitter_bug_fixed = splitter_bug_fixed_;
+}
+
+bool SplitterContext::UsingSplitter() const {
+    return splitter_infos.size() > 0 && info_count > 0 && splitter_destinations != nullptr &&
+           destinations_count > 0;
+}
+
+void SplitterContext::ClearAllNewConnectionFlag() {
+    for (s32 i = 0; i < info_count; i++) {
+        splitter_infos[i].SetNewConnectionFlag();
+    }
+}
+
+bool SplitterContext::Initialize(const BehaviorInfo& behavior,
+                                 const AudioRendererParameterInternal& params,
+                                 WorkbufferAllocator& allocator) {
+    if (behavior.IsSplitterSupported() && params.splitter_infos > 0 &&
+        params.splitter_destinations > 0) {
+        splitter_infos = allocator.Allocate<SplitterInfo>(params.splitter_infos, 0x10);
+
+        for (u32 i = 0; i < params.splitter_infos; i++) {
+            std::construct_at<SplitterInfo>(&splitter_infos[i], static_cast<s32>(i));
+        }
+
+        if (splitter_infos.size() == 0) {
+            splitter_infos = {};
+            return false;
+        }
+
+        splitter_destinations =
+            allocator.Allocate<SplitterDestinationData>(params.splitter_destinations, 0x10).data();
+
+        for (s32 i = 0; i < params.splitter_destinations; i++) {
+            std::construct_at<SplitterDestinationData>(&splitter_destinations[i], i);
+        }
+
+        if (params.splitter_destinations <= 0) {
+            splitter_infos = {};
+            splitter_destinations = nullptr;
+            return false;
+        }
+
+        Setup(splitter_infos, params.splitter_infos, splitter_destinations,
+              params.splitter_destinations, behavior.IsSplitterBugFixed());
+    }
+    return true;
+}
+
+bool SplitterContext::Update(const u8* input, u32& consumed_size) {
+    auto in_params{reinterpret_cast<const InParameterHeader*>(input)};
+
+    if (destinations_count == 0 || info_count == 0) {
+        consumed_size = 0;
+        return true;
+    }
+
+    if (in_params->magic != GetSplitterInParamHeaderMagic()) {
+        consumed_size = 0;
+        return false;
+    }
+
+    for (auto& splitter_info : splitter_infos) {
+        splitter_info.ClearNewConnectionFlag();
+    }
+
+    u32 offset{sizeof(InParameterHeader)};
+    offset = UpdateInfo(input, offset, in_params->info_count);
+    offset = UpdateData(input, offset, in_params->destination_count);
+
+    consumed_size = Common::AlignUp(offset, 0x10);
+    return true;
+}
+
+u32 SplitterContext::UpdateInfo(const u8* input, u32 offset, const u32 splitter_count) {
+    for (u32 i = 0; i < splitter_count; i++) {
+        auto info_header{reinterpret_cast<const SplitterInfo::InParameter*>(input + offset)};
+
+        if (info_header->magic != GetSplitterInfoMagic()) {
+            continue;
+        }
+
+        if (info_header->id < 0 || info_header->id > info_count) {
+            break;
+        }
+
+        auto& info{splitter_infos[info_header->id]};
+        RecomposeDestination(info, info_header);
+
+        offset += info.Update(info_header);
+    }
+
+    return offset;
+}
+
+u32 SplitterContext::UpdateData(const u8* input, u32 offset, const u32 count) {
+    for (u32 i = 0; i < count; i++) {
+        auto data_header{
+            reinterpret_cast<const SplitterDestinationData::InParameter*>(input + offset)};
+
+        if (data_header->magic != GetSplitterSendDataMagic()) {
+            continue;
+        }
+
+        if (data_header->id < 0 || data_header->id > destinations_count) {
+            continue;
+        }
+
+        splitter_destinations[data_header->id].Update(*data_header);
+        offset += sizeof(SplitterDestinationData::InParameter);
+    }
+
+    return offset;
+}
+
+void SplitterContext::UpdateInternalState() {
+    for (s32 i = 0; i < info_count; i++) {
+        splitter_infos[i].UpdateInternalState();
+    }
+}
+
+void SplitterContext::RecomposeDestination(SplitterInfo& out_info,
+                                           const SplitterInfo::InParameter* info_header) {
+    auto destination{out_info.GetData(0)};
+    while (destination != nullptr) {
+        auto dest{destination->GetNext()};
+        destination->SetNext(nullptr);
+        destination = dest;
+    }
+    out_info.SetDestinations(nullptr);
+
+    auto dest_count{info_header->destination_count};
+    if (!splitter_bug_fixed) {
+        dest_count = std::min(dest_count, GetDestCountPerInfoForCompat());
+    }
+
+    if (dest_count == 0) {
+        return;
+    }
+
+    std::span<const u32> destination_ids{reinterpret_cast<const u32*>(&info_header[1]), dest_count};
+
+    auto head{&splitter_destinations[destination_ids[0]]};
+    auto current_destination{head};
+    for (u32 i = 1; i < dest_count; i++) {
+        auto next_destination{&splitter_destinations[destination_ids[i]]};
+        current_destination->SetNext(next_destination);
+        current_destination = next_destination;
+    }
+
+    out_info.SetDestinations(head);
+    out_info.SetDestinationCount(dest_count);
+}
+
+u32 SplitterContext::GetDestCountPerInfoForCompat() const {
+    if (info_count <= 0) {
+        return 0;
+    }
+    return static_cast<u32>(destinations_count / info_count);
+}
+
+u64 SplitterContext::CalcWorkBufferSize(const BehaviorInfo& behavior,
+                                        const AudioRendererParameterInternal& params) {
+    u64 size{0};
+    if (!behavior.IsSplitterSupported()) {
+        return size;
+    }
+
+    size += params.splitter_destinations * sizeof(SplitterDestinationData) +
+            params.splitter_infos * sizeof(SplitterInfo);
+
+    if (behavior.IsSplitterBugFixed()) {
+        size += Common::AlignUp(params.splitter_destinations * sizeof(u32), 0x10);
+    }
+    return size;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/splitter/splitter_context.h b/src/audio_core/renderer/splitter/splitter_context.h
new file mode 100644
index 0000000000..cfd092b4fb
--- /dev/null
+++ b/src/audio_core/renderer/splitter/splitter_context.h
@@ -0,0 +1,189 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <span>
+
+#include "audio_core/renderer/splitter/splitter_destinations_data.h"
+#include "audio_core/renderer/splitter/splitter_info.h"
+#include "common/common_types.h"
+
+namespace AudioCore {
+struct AudioRendererParameterInternal;
+class WorkbufferAllocator;
+
+namespace AudioRenderer {
+class BehaviorInfo;
+
+/**
+ * The splitter allows much more control over how sound is mixed together.
+ * Previously, one mix can only connect to one other, and you may need
+ * more mixes (and duplicate processing) to achieve the same result.
+ * With the splitter, many-to-one and one-to-many mixing is possible.
+ * This was added in revision 2.
+ * Had a bug with incorrect numbers of destinations, fixed in revision 5.
+ */
+class SplitterContext {
+    struct InParameterHeader {
+        /* 0x00 */ u32 magic; // 'SNDH'
+        /* 0x04 */ s32 info_count;
+        /* 0x08 */ s32 destination_count;
+        /* 0x0C */ char unk0C[0x14];
+    };
+    static_assert(sizeof(InParameterHeader) == 0x20,
+                  "SplitterContext::InParameterHeader has the wrong size!");
+
+public:
+    /**
+     * Get a destination mix from the given splitter and destination index.
+     *
+     * @param splitter_id    - Splitter index to get from.
+     * @param destination_id - Destination index within the splitter.
+     * @return Pointer to the found destination. May be nullptr.
+     */
+    SplitterDestinationData* GetDesintationData(s32 splitter_id, s32 destination_id);
+
+    /**
+     * Get a splitter from the given index.
+     *
+     * @param index    - Index of the desired splitter.
+     * @return Splitter requested.
+     */
+    SplitterInfo& GetInfo(s32 index);
+
+    /**
+     * Get the total number of splitter destinations.
+     *
+     * @return Number of destiantions.
+     */
+    u32 GetDataCount() const;
+
+    /**
+     * Get the total number of splitters.
+     *
+     * @return Number of splitters.
+     */
+    u32 GetInfoCount() const;
+
+    /**
+     * Get a specific global destination.
+     *
+     * @param index - Index of the desired destination.
+     * @return The requested destination.
+     */
+    SplitterDestinationData& GetData(u32 index);
+
+    /**
+     * Check if the splitter is in use.
+     *
+     * @return True if any splitter or destination is in use, otherwise false.
+     */
+    bool UsingSplitter() const;
+
+    /**
+     * Mark all splitters as having new connections.
+     */
+    void ClearAllNewConnectionFlag();
+
+    /**
+     * Initialize the context.
+     *
+     * @param behavior - Used to check for splitter support.
+     * @param params    - Input parameters.
+     * @param allocator - Allocator used to allocate workbuffer memory.
+     */
+    bool Initialize(const BehaviorInfo& behavior, const AudioRendererParameterInternal& params,
+                    WorkbufferAllocator& allocator);
+
+    /**
+     * Update the context.
+     *
+     * @param input         - Input buffer with the new info,
+     *                        expected to point to a InParameterHeader.
+     * @param consumed_size - Output with the number of bytes consumed from input.
+     */
+    bool Update(const u8* input, u32& consumed_size);
+
+    /**
+     * Update the splitters.
+     *
+     * @param input          - Input buffer with the new info.
+     * @param offset         - Current offset within the input buffer,
+     *                         input + offset should point to a SplitterInfo::InParameter.
+     * @param splitter_count - Number of splitters in the input buffer.
+     * @return Number of bytes consumed in input.
+     */
+    u32 UpdateInfo(const u8* input, u32 offset, u32 splitter_count);
+
+    /**
+     * Update the splitters.
+     *
+     * @param input             - Input buffer with the new info.
+     * @param offset            - Current offset within the input buffer,
+     *                            input + offset should point to a
+     *                            SplitterDestinationData::InParameter.
+     * @param destination_count - Number of destinations in the input buffer.
+     * @return Number of bytes consumed in input.
+     */
+    u32 UpdateData(const u8* input, u32 offset, u32 destination_count);
+
+    /**
+     * Update the state of all destinations in all splitters.
+     */
+    void UpdateInternalState();
+
+    /**
+     * Replace the given splitter's destinations with new ones.
+     *
+     * @param out_info    - Splitter to recompose.
+     * @param info_header - Input parameters containing new destination ids.
+     */
+    void RecomposeDestination(SplitterInfo& out_info, const SplitterInfo::InParameter* info_header);
+
+    /**
+     * Old calculation for destinations, this is the thing the splitter bug fixes.
+     * Left for compatibility, and now min'd with the actual count to not bug.
+     *
+     * @return Number of splitter destinations.
+     */
+    u32 GetDestCountPerInfoForCompat() const;
+
+    /**
+     * Calculate the size of the required workbuffer for splitters and destinations.
+     *
+     * @param behavior - Used to check splitter features.
+     * @param params    - Input parameters with splitter/destination counts.
+     * @return Required buffer size.
+     */
+    static u64 CalcWorkBufferSize(const BehaviorInfo& behavior,
+                                  const AudioRendererParameterInternal& params);
+
+private:
+    /**
+     * Setup the context.
+     *
+     * @param splitter_infos        - Workbuffer for splitters.
+     * @param splitter_info_count   - Number of splitters in the workbuffer.
+     * @param splitter_destinations - Workbuffer for splitter destinations.
+     * @param destination_count     - Number of destinations in the workbuffer.
+     * @param splitter_bug_fixed    - Is the splitter bug fixed?
+     */
+    void Setup(std::span<SplitterInfo> splitter_infos, u32 splitter_info_count,
+               SplitterDestinationData* splitter_destinations, u32 destination_count,
+               bool splitter_bug_fixed);
+
+    /// Workbuffer for splitters
+    std::span<SplitterInfo> splitter_infos{};
+    /// Number of splitters in buffer
+    s32 info_count{};
+    /// Workbuffer for destinations
+    SplitterDestinationData* splitter_destinations{};
+    /// Number of destinations in buffer
+    s32 destinations_count{};
+    /// Is the splitter bug fixed?
+    bool splitter_bug_fixed{};
+};
+
+} // namespace AudioRenderer
+} // namespace AudioCore
diff --git a/src/audio_core/renderer/splitter/splitter_destinations_data.cpp b/src/audio_core/renderer/splitter/splitter_destinations_data.cpp
new file mode 100644
index 0000000000..b27d44896d
--- /dev/null
+++ b/src/audio_core/renderer/splitter/splitter_destinations_data.cpp
@@ -0,0 +1,87 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/splitter/splitter_destinations_data.h"
+
+namespace AudioCore::AudioRenderer {
+
+SplitterDestinationData::SplitterDestinationData(const s32 id_) : id{id_} {}
+
+void SplitterDestinationData::ClearMixVolume() {
+    mix_volumes.fill(0.0f);
+    prev_mix_volumes.fill(0.0f);
+}
+
+s32 SplitterDestinationData::GetId() const {
+    return id;
+}
+
+bool SplitterDestinationData::IsConfigured() const {
+    return in_use && destination_id != UnusedMixId;
+}
+
+s32 SplitterDestinationData::GetMixId() const {
+    return destination_id;
+}
+
+f32 SplitterDestinationData::GetMixVolume(const u32 index) const {
+    if (index >= mix_volumes.size()) {
+        LOG_ERROR(Service_Audio, "SplitterDestinationData::GetMixVolume Invalid index {}", index);
+        return 0.0f;
+    }
+    return mix_volumes[index];
+}
+
+std::span<f32> SplitterDestinationData::GetMixVolume() {
+    return mix_volumes;
+}
+
+f32 SplitterDestinationData::GetMixVolumePrev(const u32 index) const {
+    if (index >= prev_mix_volumes.size()) {
+        LOG_ERROR(Service_Audio, "SplitterDestinationData::GetMixVolumePrev Invalid index {}",
+                  index);
+        return 0.0f;
+    }
+    return prev_mix_volumes[index];
+}
+
+std::span<f32> SplitterDestinationData::GetMixVolumePrev() {
+    return prev_mix_volumes;
+}
+
+void SplitterDestinationData::Update(const InParameter& params) {
+    if (params.id != id || params.magic != GetSplitterSendDataMagic()) {
+        return;
+    }
+
+    destination_id = params.mix_id;
+    mix_volumes = params.mix_volumes;
+
+    if (!in_use && params.in_use) {
+        prev_mix_volumes = mix_volumes;
+        need_update = false;
+    }
+
+    in_use = params.in_use;
+}
+
+void SplitterDestinationData::MarkAsNeedToUpdateInternalState() {
+    need_update = true;
+}
+
+void SplitterDestinationData::UpdateInternalState() {
+    if (in_use && need_update) {
+        prev_mix_volumes = mix_volumes;
+    }
+    need_update = false;
+}
+
+SplitterDestinationData* SplitterDestinationData::GetNext() const {
+    return next;
+}
+
+void SplitterDestinationData::SetNext(SplitterDestinationData* next_) {
+    next = next_;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/splitter/splitter_destinations_data.h b/src/audio_core/renderer/splitter/splitter_destinations_data.h
new file mode 100644
index 0000000000..bd3d55748a
--- /dev/null
+++ b/src/audio_core/renderer/splitter/splitter_destinations_data.h
@@ -0,0 +1,135 @@
+// 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"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * Represents a mixing node, can be connected to a previous and next destination forming a chain
+ * that a certain mix buffer will pass through to output.
+ */
+class SplitterDestinationData {
+public:
+    struct InParameter {
+        /* 0x00 */ u32 magic; // 'SNDD'
+        /* 0x04 */ s32 id;
+        /* 0x08 */ std::array<f32, MaxMixBuffers> mix_volumes;
+        /* 0x68 */ u32 mix_id;
+        /* 0x6C */ bool in_use;
+    };
+    static_assert(sizeof(InParameter) == 0x70,
+                  "SplitterDestinationData::InParameter has the wrong size!");
+
+    SplitterDestinationData(s32 id);
+
+    /**
+     * Reset the mix volumes for this destination.
+     */
+    void ClearMixVolume();
+
+    /**
+     * Get the id of this destination.
+     *
+     * @return Id for this destination.
+     */
+    s32 GetId() const;
+
+    /**
+     * Check if this destination is correctly configured.
+     *
+     * @return True if configured, otherwise false.
+     */
+    bool IsConfigured() const;
+
+    /**
+     * Get the mix id for this destination.
+     *
+     * @return Mix id for this destination.
+     */
+    s32 GetMixId() const;
+
+    /**
+     * Get the current mix volume of a given index in this destination.
+     *
+     * @param index - Mix buffer index to get the volume for.
+     * @return Current volume of the specified mix.
+     */
+    f32 GetMixVolume(u32 index) const;
+
+    /**
+     * Get the current mix volumes for all mix buffers in this destination.
+     *
+     * @return Span of current mix buffer volumes.
+     */
+    std::span<f32> GetMixVolume();
+
+    /**
+     * Get the previous mix volume of a given index in this destination.
+     *
+     * @param index - Mix buffer index to get the volume for.
+     * @return Previous volume of the specified mix.
+     */
+    f32 GetMixVolumePrev(u32 index) const;
+
+    /**
+     * Get the previous mix volumes for all mix buffers in this destination.
+     *
+     * @return Span of previous mix buffer volumes.
+     */
+    std::span<f32> GetMixVolumePrev();
+
+    /**
+     * Update this destination.
+     *
+     * @param params - Inpout parameters to update the destination.
+     */
+    void Update(const InParameter& params);
+
+    /**
+     * Mark this destination as needing its volumes updated.
+     */
+    void MarkAsNeedToUpdateInternalState();
+
+    /**
+     * Copy current volumes to previous if an update is required.
+     */
+    void UpdateInternalState();
+
+    /**
+     * Get the next destination in the mix chain.
+     *
+     * @return The next splitter destination, may be nullptr if this is the last in the chain.
+     */
+    SplitterDestinationData* GetNext() const;
+
+    /**
+     * Set the next destination in the mix chain.
+     *
+     * @param next - Destination this one is to be connected to.
+     */
+    void SetNext(SplitterDestinationData* next);
+
+private:
+    /// Id of this destination
+    const s32 id;
+    /// Mix id this destination represents
+    s32 destination_id{UnusedMixId};
+    /// Current mix volumes
+    std::array<f32, MaxMixBuffers> mix_volumes{0.0f};
+    /// Previous mix volumes
+    std::array<f32, MaxMixBuffers> prev_mix_volumes{0.0f};
+    /// Next destination in the mix chain
+    SplitterDestinationData* next{};
+    /// Is this destiantion in use?
+    bool in_use{};
+    /// Does this destiantion need its volumes updated?
+    bool need_update{};
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/splitter/splitter_info.cpp b/src/audio_core/renderer/splitter/splitter_info.cpp
new file mode 100644
index 0000000000..1aee6720b7
--- /dev/null
+++ b/src/audio_core/renderer/splitter/splitter_info.cpp
@@ -0,0 +1,79 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/splitter/splitter_info.h"
+
+namespace AudioCore::AudioRenderer {
+
+SplitterInfo::SplitterInfo(const s32 id_) : id{id_} {}
+
+void SplitterInfo::InitializeInfos(SplitterInfo* splitters, const u32 count) {
+    if (splitters == nullptr) {
+        return;
+    }
+
+    for (u32 i = 0; i < count; i++) {
+        auto& splitter{splitters[i]};
+        splitter.destinations = nullptr;
+        splitter.destination_count = 0;
+        splitter.has_new_connection = true;
+    }
+}
+
+u32 SplitterInfo::Update(const InParameter* params) {
+    if (params->id != id) {
+        return 0;
+    }
+    sample_rate = params->sample_rate;
+    has_new_connection = true;
+    return static_cast<u32>((sizeof(InParameter) + 3 * sizeof(s32)) +
+                            params->destination_count * sizeof(s32));
+}
+
+SplitterDestinationData* SplitterInfo::GetData(const u32 destination_id) {
+    auto out_destination{destinations};
+    u32 i{0};
+    while (i < destination_id) {
+        if (out_destination == nullptr) {
+            break;
+        }
+        out_destination = out_destination->GetNext();
+        i++;
+    }
+
+    return out_destination;
+}
+
+u32 SplitterInfo::GetDestinationCount() const {
+    return destination_count;
+}
+
+void SplitterInfo::SetDestinationCount(const u32 count) {
+    destination_count = count;
+}
+
+bool SplitterInfo::HasNewConnection() const {
+    return has_new_connection;
+}
+
+void SplitterInfo::ClearNewConnectionFlag() {
+    has_new_connection = false;
+}
+
+void SplitterInfo::SetNewConnectionFlag() {
+    has_new_connection = true;
+}
+
+void SplitterInfo::UpdateInternalState() {
+    auto destination{destinations};
+    while (destination != nullptr) {
+        destination->UpdateInternalState();
+        destination = destination->GetNext();
+    }
+}
+
+void SplitterInfo::SetDestinations(SplitterDestinationData* destinations_) {
+    destinations = destinations_;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/splitter/splitter_info.h b/src/audio_core/renderer/splitter/splitter_info.h
new file mode 100644
index 0000000000..d1d75064cc
--- /dev/null
+++ b/src/audio_core/renderer/splitter/splitter_info.h
@@ -0,0 +1,107 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "audio_core/renderer/splitter/splitter_destinations_data.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * Represents a splitter, wraps multiple output destinations to split an input mix into.
+ */
+class SplitterInfo {
+public:
+    struct InParameter {
+        /* 0x00 */ u32 magic; // 'SNDI'
+        /* 0x04 */ s32 id;
+        /* 0x08 */ u32 sample_rate;
+        /* 0x0C */ u32 destination_count;
+    };
+    static_assert(sizeof(InParameter) == 0x10, "SplitterInfo::InParameter has the wrong size!");
+
+    explicit SplitterInfo(s32 id);
+
+    /**
+     * Initialize the given splitters.
+     *
+     * @param splitters - Splitters to initialize.
+     * @param count     - Number of splitters given.
+     */
+    static void InitializeInfos(SplitterInfo* splitters, u32 count);
+
+    /**
+     * Update this splitter.
+     *
+     * @param params - Input parameters to update with.
+     * @return The size in bytes of this splitter.
+     */
+    u32 Update(const InParameter* params);
+
+    /**
+     * Get a destination in this splitter.
+     *
+     * @param id - Destination id to get.
+     * @return Pointer to the destination, may be nullptr.
+     */
+    SplitterDestinationData* GetData(u32 id);
+
+    /**
+     * Get the number of destinations in this splitter.
+     *
+     * @return The number of destiantions.
+     */
+    u32 GetDestinationCount() const;
+
+    /**
+     * Set the number of destinations in this splitter.
+     *
+     * @param count - The new number of destiantions.
+     */
+    void SetDestinationCount(u32 count);
+
+    /**
+     * Check if the splitter has a new connection.
+     *
+     * @return True if there is a new connection, otherwise false.
+     */
+    bool HasNewConnection() const;
+
+    /**
+     * Reset the new connection flag.
+     */
+    void ClearNewConnectionFlag();
+
+    /**
+     * Mark as having a new connection.
+     */
+    void SetNewConnectionFlag();
+
+    /**
+     * Update the state of all destinations.
+     */
+    void UpdateInternalState();
+
+    /**
+     * Set this splitter's destinations.
+     *
+     * @param destinations - The new destination list for this splitter.
+     */
+    void SetDestinations(SplitterDestinationData* destinations);
+
+private:
+    /// Id of this splitter
+    s32 id;
+    /// Sample rate of this splitter
+    u32 sample_rate{};
+    /// Number of destinations in this splitter
+    u32 destination_count{};
+    /// Does this splitter have a new connection?
+    bool has_new_connection{true};
+    /// Pointer to the destinations of this splitter
+    SplitterDestinationData* destinations{};
+    /// Number of channels this splitter manages
+    u32 channel_count{};
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/system.cpp b/src/audio_core/renderer/system.cpp
new file mode 100644
index 0000000000..7a217969e9
--- /dev/null
+++ b/src/audio_core/renderer/system.cpp
@@ -0,0 +1,802 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <chrono>
+#include <span>
+
+#include "audio_core/audio_core.h"
+#include "audio_core/common/audio_renderer_parameter.h"
+#include "audio_core/common/common.h"
+#include "audio_core/common/feature_support.h"
+#include "audio_core/common/workbuffer_allocator.h"
+#include "audio_core/renderer/adsp/adsp.h"
+#include "audio_core/renderer/behavior/info_updater.h"
+#include "audio_core/renderer/command/command_buffer.h"
+#include "audio_core/renderer/command/command_generator.h"
+#include "audio_core/renderer/command/command_list_header.h"
+#include "audio_core/renderer/effect/effect_info_base.h"
+#include "audio_core/renderer/effect/effect_result_state.h"
+#include "audio_core/renderer/memory/memory_pool_info.h"
+#include "audio_core/renderer/memory/pool_mapper.h"
+#include "audio_core/renderer/mix/mix_info.h"
+#include "audio_core/renderer/nodes/edge_matrix.h"
+#include "audio_core/renderer/nodes/node_states.h"
+#include "audio_core/renderer/sink/sink_info_base.h"
+#include "audio_core/renderer/system.h"
+#include "audio_core/renderer/upsampler/upsampler_info.h"
+#include "audio_core/renderer/voice/voice_channel_resource.h"
+#include "audio_core/renderer/voice/voice_info.h"
+#include "audio_core/renderer/voice/voice_state.h"
+#include "common/alignment.h"
+#include "core/core.h"
+#include "core/core_timing.h"
+#include "core/hle/kernel/k_event.h"
+#include "core/hle/kernel/k_transfer_memory.h"
+#include "core/memory.h"
+
+namespace AudioCore::AudioRenderer {
+
+u64 System::GetWorkBufferSize(const AudioRendererParameterInternal& params) {
+    BehaviorInfo behavior;
+    behavior.SetUserLibRevision(params.revision);
+
+    u64 size{0};
+
+    size += Common::AlignUp(params.mixes * sizeof(s32), 0x40);
+    size += params.sub_mixes * MaxEffects * sizeof(s32);
+    size += (params.sub_mixes + 1) * sizeof(MixInfo);
+    size += params.voices * (sizeof(VoiceInfo) + sizeof(VoiceChannelResource) + sizeof(VoiceState));
+    size += Common::AlignUp((params.sub_mixes + 1) * sizeof(MixInfo*), 0x10);
+    size += Common::AlignUp(params.voices * sizeof(VoiceInfo*), 0x10);
+    size += Common::AlignUp(((params.sinks + params.sub_mixes) * TargetSampleCount * sizeof(s32) +
+                             params.sample_count * sizeof(s32)) *
+                                (params.mixes + MaxChannels),
+                            0x40);
+
+    if (behavior.IsSplitterSupported()) {
+        const auto node_size{NodeStates::GetWorkBufferSize(params.sub_mixes + 1)};
+        const auto edge_size{EdgeMatrix::GetWorkBufferSize(params.sub_mixes + 1)};
+        size += Common::AlignUp(node_size + edge_size, 0x10);
+    }
+
+    size += SplitterContext::CalcWorkBufferSize(behavior, params);
+    size += (params.effects + params.voices * MaxWaveBuffers) * sizeof(MemoryPoolInfo);
+
+    if (behavior.IsEffectInfoVersion2Supported()) {
+        size += params.effects * sizeof(EffectResultState);
+    }
+    size += 0x50;
+
+    size = Common::AlignUp(size, 0x40);
+
+    size += (params.sinks + params.sub_mixes) * sizeof(UpsamplerInfo);
+    size += params.effects * sizeof(EffectInfoBase);
+    size += Common::AlignUp(params.voices * sizeof(VoiceState), 0x40);
+    size += params.sinks * sizeof(SinkInfoBase);
+
+    if (behavior.IsEffectInfoVersion2Supported()) {
+        size += params.effects * sizeof(EffectResultState);
+    }
+
+    if (params.perf_frames > 0) {
+        auto perf_size{PerformanceManager::GetRequiredBufferSizeForPerformanceMetricsPerFrame(
+            behavior, params)};
+        size += Common::AlignUp(perf_size * (params.perf_frames + 1) + 0xC0, 0x100);
+    }
+
+    if (behavior.IsVariadicCommandBufferSizeSupported()) {
+        size += CommandGenerator::CalculateCommandBufferSize(behavior, params) + (0x40 - 1) * 2;
+    } else {
+        size += 0x18000 + (0x40 - 1) * 2;
+    }
+
+    size = Common::AlignUp(size, 0x1000);
+    return size;
+}
+
+System::System(Core::System& core_, Kernel::KEvent* adsp_rendered_event_)
+    : core{core_}, adsp{core.AudioCore().GetADSP()}, adsp_rendered_event{adsp_rendered_event_} {}
+
+Result System::Initialize(const AudioRendererParameterInternal& params,
+                          Kernel::KTransferMemory* transfer_memory, const u64 transfer_memory_size,
+                          const u32 process_handle_, const u64 applet_resource_user_id_,
+                          const s32 session_id_) {
+    if (!CheckValidRevision(params.revision)) {
+        return Service::Audio::ERR_INVALID_REVISION;
+    }
+
+    if (GetWorkBufferSize(params) > transfer_memory_size) {
+        return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE;
+    }
+
+    if (process_handle_ == 0) {
+        return Service::Audio::ERR_INVALID_PROCESS_HANDLE;
+    }
+
+    behavior.SetUserLibRevision(params.revision);
+
+    process_handle = process_handle_;
+    applet_resource_user_id = applet_resource_user_id_;
+    session_id = session_id_;
+
+    sample_rate = params.sample_rate;
+    sample_count = params.sample_count;
+    mix_buffer_count = static_cast<s16>(params.mixes);
+    voice_channels = MaxChannels;
+    upsampler_count = params.sinks + params.sub_mixes;
+    memory_pool_count = params.effects + params.voices * MaxWaveBuffers;
+    render_device = params.rendering_device;
+    execution_mode = params.execution_mode;
+
+    core.Memory().ZeroBlock(*core.Kernel().CurrentProcess(), transfer_memory->GetSourceAddress(),
+                            transfer_memory_size);
+
+    // Note: We're not actually using the transfer memory because it's a pain to code for.
+    // Allocate the memory normally instead and hope the game doesn't try to read anything back
+    workbuffer = std::make_unique<u8[]>(transfer_memory_size);
+    workbuffer_size = transfer_memory_size;
+
+    PoolMapper pool_mapper(process_handle, false);
+    pool_mapper.InitializeSystemPool(memory_pool_info, workbuffer.get(), workbuffer_size);
+
+    WorkbufferAllocator allocator({workbuffer.get(), workbuffer_size}, workbuffer_size);
+
+    samples_workbuffer =
+        allocator.Allocate<s32>((voice_channels + mix_buffer_count) * sample_count, 0x10);
+    if (samples_workbuffer.empty()) {
+        return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE;
+    }
+
+    auto upsampler_workbuffer{allocator.Allocate<s32>(
+        (voice_channels + mix_buffer_count) * TargetSampleCount * upsampler_count, 0x10)};
+    if (upsampler_workbuffer.empty()) {
+        return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE;
+    }
+
+    depop_buffer =
+        allocator.Allocate<s32>(Common::AlignUp(static_cast<u32>(mix_buffer_count), 0x40), 0x40);
+    if (depop_buffer.empty()) {
+        return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE;
+    }
+
+    // invalidate samples_workbuffer DSP cache
+
+    auto voice_infos{allocator.Allocate<VoiceInfo>(params.voices, 0x10)};
+    for (auto& voice_info : voice_infos) {
+        std::construct_at<VoiceInfo>(&voice_info);
+    }
+
+    if (voice_infos.empty()) {
+        return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE;
+    }
+
+    auto sorted_voice_infos{allocator.Allocate<VoiceInfo*>(params.voices, 0x10)};
+    if (sorted_voice_infos.empty()) {
+        return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE;
+    }
+
+    std::memset(sorted_voice_infos.data(), 0, sorted_voice_infos.size_bytes());
+
+    auto voice_channel_resources{allocator.Allocate<VoiceChannelResource>(params.voices, 0x10)};
+    u32 i{0};
+    for (auto& voice_channel_resource : voice_channel_resources) {
+        std::construct_at<VoiceChannelResource>(&voice_channel_resource, i++);
+    }
+
+    if (voice_channel_resources.empty()) {
+        return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE;
+    }
+
+    auto voice_cpu_states{allocator.Allocate<VoiceState>(params.voices, 0x10)};
+    if (voice_cpu_states.empty()) {
+        return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE;
+    }
+
+    for (auto& voice_state : voice_cpu_states) {
+        voice_state = {};
+    }
+
+    auto mix_infos{allocator.Allocate<MixInfo>(params.sub_mixes + 1, 0x10)};
+
+    if (mix_infos.empty()) {
+        return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE;
+    }
+
+    u32 effect_process_order_count{0};
+    std::span<s32> effect_process_order_buffer{};
+
+    if (params.effects > 0) {
+        effect_process_order_count = params.effects * (params.sub_mixes + 1);
+        effect_process_order_buffer = allocator.Allocate<s32>(effect_process_order_count, 0x10);
+        if (effect_process_order_buffer.empty()) {
+            return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE;
+        }
+    }
+
+    i = 0;
+    for (auto& mix_info : mix_infos) {
+        std::construct_at<MixInfo>(
+            &mix_info, effect_process_order_buffer.subspan(i * params.effects, params.effects),
+            params.effects, this->behavior);
+        i++;
+    }
+
+    auto sorted_mix_infos{allocator.Allocate<MixInfo*>(params.sub_mixes + 1, 0x10)};
+    if (sorted_mix_infos.empty()) {
+        return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE;
+    }
+
+    std::memset(sorted_mix_infos.data(), 0, sorted_mix_infos.size_bytes());
+
+    if (behavior.IsSplitterSupported()) {
+        u64 node_state_size{NodeStates::GetWorkBufferSize(params.sub_mixes + 1)};
+        u64 edge_matrix_size{EdgeMatrix::GetWorkBufferSize(params.sub_mixes + 1)};
+
+        auto node_states_workbuffer{allocator.Allocate<u8>(node_state_size, 1)};
+        auto edge_matrix_workbuffer{allocator.Allocate<u8>(edge_matrix_size, 1)};
+
+        if (node_states_workbuffer.empty() || edge_matrix_workbuffer.size() == 0) {
+            return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE;
+        }
+
+        mix_context.Initialize(sorted_mix_infos, mix_infos, params.sub_mixes + 1,
+                               effect_process_order_buffer, effect_process_order_count,
+                               node_states_workbuffer, node_state_size, edge_matrix_workbuffer,
+                               edge_matrix_size);
+    } else {
+        mix_context.Initialize(sorted_mix_infos, mix_infos, params.sub_mixes + 1,
+                               effect_process_order_buffer, effect_process_order_count, {}, 0, {},
+                               0);
+    }
+
+    upsampler_manager = allocator.Allocate<UpsamplerManager>(1, 0x10).data();
+    if (upsampler_manager == nullptr) {
+        return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE;
+    }
+
+    memory_pool_workbuffer = allocator.Allocate<MemoryPoolInfo>(memory_pool_count, 0x10);
+    for (auto& memory_pool : memory_pool_workbuffer) {
+        std::construct_at<MemoryPoolInfo>(&memory_pool, MemoryPoolInfo::Location::DSP);
+    }
+
+    if (memory_pool_workbuffer.empty() && memory_pool_count > 0) {
+        return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE;
+    }
+
+    if (!splitter_context.Initialize(behavior, params, allocator)) {
+        return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE;
+    }
+
+    std::span<EffectResultState> effect_result_states_cpu{};
+    if (behavior.IsEffectInfoVersion2Supported() && params.effects > 0) {
+        effect_result_states_cpu = allocator.Allocate<EffectResultState>(params.effects, 0x10);
+        if (effect_result_states_cpu.empty()) {
+            return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE;
+        }
+        std::memset(effect_result_states_cpu.data(), 0, effect_result_states_cpu.size_bytes());
+    }
+
+    allocator.Align(0x40);
+
+    unk_2B0 = allocator.GetSize() - allocator.GetCurrentOffset();
+    unk_2A8 = {&workbuffer[allocator.GetCurrentOffset()], unk_2B0};
+
+    upsampler_infos = allocator.Allocate<UpsamplerInfo>(upsampler_count, 0x40);
+    for (auto& upsampler_info : upsampler_infos) {
+        std::construct_at<UpsamplerInfo>(&upsampler_info);
+    }
+
+    std::construct_at<UpsamplerManager>(upsampler_manager, upsampler_count, upsampler_infos,
+                                        upsampler_workbuffer);
+
+    if (upsampler_infos.empty()) {
+        return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE;
+    }
+
+    auto effect_infos{allocator.Allocate<EffectInfoBase>(params.effects, 0x40)};
+    for (auto& effect_info : effect_infos) {
+        std::construct_at<EffectInfoBase>(&effect_info);
+    }
+
+    if (effect_infos.empty() && params.effects > 0) {
+        return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE;
+    }
+
+    std::span<EffectResultState> effect_result_states_dsp{};
+    if (behavior.IsEffectInfoVersion2Supported() && params.effects > 0) {
+        effect_result_states_dsp = allocator.Allocate<EffectResultState>(params.effects, 0x40);
+        if (effect_result_states_dsp.empty()) {
+            return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE;
+        }
+        std::memset(effect_result_states_dsp.data(), 0, effect_result_states_dsp.size_bytes());
+    }
+
+    effect_context.Initialize(effect_infos, params.effects, effect_result_states_cpu,
+                              effect_result_states_dsp, effect_result_states_dsp.size());
+
+    auto sinks{allocator.Allocate<SinkInfoBase>(params.sinks, 0x10)};
+    for (auto& sink : sinks) {
+        std::construct_at<SinkInfoBase>(&sink);
+    }
+
+    if (sinks.empty()) {
+        return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE;
+    }
+
+    sink_context.Initialize(sinks, params.sinks);
+
+    auto voice_dsp_states{allocator.Allocate<VoiceState>(params.voices, 0x40)};
+    if (voice_dsp_states.empty()) {
+        return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE;
+    }
+
+    for (auto& voice_state : voice_dsp_states) {
+        voice_state = {};
+    }
+
+    voice_context.Initialize(sorted_voice_infos, voice_infos, voice_channel_resources,
+                             voice_cpu_states, voice_dsp_states, params.voices);
+
+    if (params.perf_frames > 0) {
+        const auto perf_workbuffer_size{
+            PerformanceManager::GetRequiredBufferSizeForPerformanceMetricsPerFrame(behavior,
+                                                                                   params) *
+                (params.perf_frames + 1) +
+            0xC};
+        performance_workbuffer = allocator.Allocate<u8>(perf_workbuffer_size, 0x40);
+        if (performance_workbuffer.empty()) {
+            return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE;
+        }
+        std::memset(performance_workbuffer.data(), 0, performance_workbuffer.size_bytes());
+        performance_manager.Initialize(performance_workbuffer, performance_workbuffer.size_bytes(),
+                                       params, behavior, memory_pool_info);
+    }
+
+    render_time_limit_percent = 100;
+    drop_voice = params.voice_drop_enabled && params.execution_mode == ExecutionMode::Auto;
+
+    allocator.Align(0x40);
+    command_workbuffer_size = allocator.GetRemainingSize();
+    command_workbuffer = allocator.Allocate<u8>(command_workbuffer_size, 0x40);
+    if (command_workbuffer.empty()) {
+        return Service::Audio::ERR_INSUFFICIENT_BUFFER_SIZE;
+    }
+
+    command_buffer_size = 0;
+    reset_command_buffers = true;
+
+    // nn::audio::dsp::FlushDataCache(transferMemory, transferMemorySize);
+
+    if (behavior.IsCommandProcessingTimeEstimatorVersion5Supported()) {
+        command_processing_time_estimator =
+            std::make_unique<CommandProcessingTimeEstimatorVersion5>(sample_count,
+                                                                     mix_buffer_count);
+    } else if (behavior.IsCommandProcessingTimeEstimatorVersion4Supported()) {
+        command_processing_time_estimator =
+            std::make_unique<CommandProcessingTimeEstimatorVersion4>(sample_count,
+                                                                     mix_buffer_count);
+    } else if (behavior.IsCommandProcessingTimeEstimatorVersion3Supported()) {
+        command_processing_time_estimator =
+            std::make_unique<CommandProcessingTimeEstimatorVersion3>(sample_count,
+                                                                     mix_buffer_count);
+    } else if (behavior.IsCommandProcessingTimeEstimatorVersion2Supported()) {
+        command_processing_time_estimator =
+            std::make_unique<CommandProcessingTimeEstimatorVersion2>(sample_count,
+                                                                     mix_buffer_count);
+    } else {
+        command_processing_time_estimator =
+            std::make_unique<CommandProcessingTimeEstimatorVersion1>(sample_count,
+                                                                     mix_buffer_count);
+    }
+
+    initialized = true;
+    return ResultSuccess;
+}
+
+void System::Finalize() {
+    if (!initialized) {
+        return;
+    }
+
+    if (active) {
+        Stop();
+    }
+
+    applet_resource_user_id = 0;
+
+    PoolMapper pool_mapper(process_handle, false);
+    pool_mapper.Unmap(memory_pool_info);
+
+    if (process_handle) {
+        pool_mapper.ClearUseState(memory_pool_workbuffer, memory_pool_count);
+        for (auto& memory_pool : memory_pool_workbuffer) {
+            if (memory_pool.IsMapped()) {
+                pool_mapper.Unmap(memory_pool);
+            }
+        }
+
+        // dsp::ProcessCleanup
+        // close handle
+    }
+    initialized = false;
+}
+
+void System::Start() {
+    std::scoped_lock l{lock};
+    frames_elapsed = 0;
+    state = State::Started;
+    active = true;
+}
+
+void System::Stop() {
+    {
+        std::scoped_lock l{lock};
+        state = State::Stopped;
+        active = false;
+    }
+
+    if (execution_mode == ExecutionMode::Auto) {
+        // Should wait for the system to terminate here, but core timing (should have) already
+        // stopped, so this isn't needed. Find a way to make this definite.
+
+        // terminate_event.Wait();
+    }
+}
+
+Result System::Update(std::span<const u8> input, std::span<u8> performance, std::span<u8> output) {
+    std::scoped_lock l{lock};
+
+    const auto start_time{core.CoreTiming().GetClockTicks()};
+
+    InfoUpdater info_updater(input, output, process_handle, behavior);
+
+    auto result{info_updater.UpdateBehaviorInfo(behavior)};
+    if (result.IsError()) {
+        LOG_ERROR(Service_Audio, "Failed to update BehaviorInfo!");
+        return result;
+    }
+
+    result = info_updater.UpdateMemoryPools(memory_pool_workbuffer, memory_pool_count);
+    if (result.IsError()) {
+        LOG_ERROR(Service_Audio, "Failed to update MemoryPools!");
+        return result;
+    }
+
+    result = info_updater.UpdateVoiceChannelResources(voice_context);
+    if (result.IsError()) {
+        LOG_ERROR(Service_Audio, "Failed to update VoiceChannelResources!");
+        return result;
+    }
+
+    result = info_updater.UpdateVoices(voice_context, memory_pool_workbuffer, memory_pool_count);
+    if (result.IsError()) {
+        LOG_ERROR(Service_Audio, "Failed to update Voices!");
+        return result;
+    }
+
+    result = info_updater.UpdateEffects(effect_context, active, memory_pool_workbuffer,
+                                        memory_pool_count);
+    if (result.IsError()) {
+        LOG_ERROR(Service_Audio, "Failed to update Effects!");
+        return result;
+    }
+
+    if (behavior.IsSplitterSupported()) {
+        result = info_updater.UpdateSplitterInfo(splitter_context);
+        if (result.IsError()) {
+            LOG_ERROR(Service_Audio, "Failed to update SplitterInfo!");
+            return result;
+        }
+    }
+
+    result =
+        info_updater.UpdateMixes(mix_context, mix_buffer_count, effect_context, splitter_context);
+    if (result.IsError()) {
+        LOG_ERROR(Service_Audio, "Failed to update Mixes!");
+        return result;
+    }
+
+    result = info_updater.UpdateSinks(sink_context, memory_pool_workbuffer, memory_pool_count);
+    if (result.IsError()) {
+        LOG_ERROR(Service_Audio, "Failed to update Sinks!");
+        return result;
+    }
+
+    PerformanceManager* perf_manager{nullptr};
+    if (performance_manager.IsInitialized()) {
+        perf_manager = &performance_manager;
+    }
+
+    result =
+        info_updater.UpdatePerformanceBuffer(performance, performance.size_bytes(), perf_manager);
+    if (result.IsError()) {
+        LOG_ERROR(Service_Audio, "Failed to update PerformanceBuffer!");
+        return result;
+    }
+
+    result = info_updater.UpdateErrorInfo(behavior);
+    if (result.IsError()) {
+        LOG_ERROR(Service_Audio, "Failed to update ErrorInfo!");
+        return result;
+    }
+
+    if (behavior.IsElapsedFrameCountSupported()) {
+        result = info_updater.UpdateRendererInfo(frames_elapsed);
+        if (result.IsError()) {
+            LOG_ERROR(Service_Audio, "Failed to update RendererInfo!");
+            return result;
+        }
+    }
+
+    result = info_updater.CheckConsumedSize();
+    if (result.IsError()) {
+        LOG_ERROR(Service_Audio, "Invalid consume size!");
+        return result;
+    }
+
+    adsp_rendered_event->GetWritableEvent().Clear();
+    num_times_updated++;
+
+    const auto end_time{core.CoreTiming().GetClockTicks()};
+    ticks_spent_updating += end_time - start_time;
+
+    return ResultSuccess;
+}
+
+u32 System::GetRenderingTimeLimit() const {
+    return render_time_limit_percent;
+}
+
+void System::SetRenderingTimeLimit(const u32 limit) {
+    render_time_limit_percent = limit;
+}
+
+u32 System::GetSessionId() const {
+    return session_id;
+}
+
+u32 System::GetSampleRate() const {
+    return sample_rate;
+}
+
+u32 System::GetSampleCount() const {
+    return sample_count;
+}
+
+u32 System::GetMixBufferCount() const {
+    return mix_buffer_count;
+}
+
+ExecutionMode System::GetExecutionMode() const {
+    return execution_mode;
+}
+
+u32 System::GetRenderingDevice() const {
+    return render_device;
+}
+
+bool System::IsActive() const {
+    return active;
+}
+
+void System::SendCommandToDsp() {
+    std::scoped_lock l{lock};
+
+    if (initialized) {
+        if (active) {
+            terminate_event.Reset();
+            const auto remaining_command_count{adsp.GetRemainCommandCount(session_id)};
+            u64 command_size{0};
+
+            if (remaining_command_count) {
+                adsp_behind = true;
+                command_size = command_buffer_size;
+            } else {
+                command_size = GenerateCommand(command_workbuffer, command_workbuffer_size);
+            }
+
+            auto translated_addr{
+                memory_pool_info.Translate(CpuAddr(command_workbuffer.data()), command_size)};
+
+            auto time_limit_percent{70.0f};
+            if (behavior.IsAudioRendererProcessingTimeLimit80PercentSupported()) {
+                time_limit_percent = 80.0f;
+            } else if (behavior.IsAudioRendererProcessingTimeLimit75PercentSupported()) {
+                time_limit_percent = 75.0f;
+            } else {
+                // result ignored and 70 is used anyway
+                behavior.IsAudioRendererProcessingTimeLimit70PercentSupported();
+                time_limit_percent = 70.0f;
+            }
+
+            ADSP::CommandBuffer command_buffer{
+                .buffer{translated_addr},
+                .size{command_size},
+                .time_limit{
+                    static_cast<u64>((time_limit_percent / 100) * 2'880'000.0 *
+                                     (static_cast<f32>(render_time_limit_percent) / 100.0f))},
+                .remaining_command_count{remaining_command_count},
+                .reset_buffers{reset_command_buffers},
+                .applet_resource_user_id{applet_resource_user_id},
+                .render_time_taken{adsp.GetRenderTimeTaken(session_id)},
+            };
+
+            adsp.SendCommandBuffer(session_id, command_buffer);
+            reset_command_buffers = false;
+            command_buffer_size = command_size;
+            if (remaining_command_count == 0) {
+                adsp_rendered_event->GetWritableEvent().Signal();
+            }
+        } else {
+            adsp.ClearRemainCount(session_id);
+            terminate_event.Set();
+        }
+    }
+}
+
+u64 System::GenerateCommand(std::span<u8> in_command_buffer,
+                            [[maybe_unused]] const u64 command_buffer_size_) {
+    PoolMapper::ClearUseState(memory_pool_workbuffer, memory_pool_count);
+    const auto start_time{core.CoreTiming().GetClockTicks()};
+
+    auto command_list_header{reinterpret_cast<CommandListHeader*>(in_command_buffer.data())};
+
+    command_list_header->buffer_count = static_cast<s16>(voice_channels + mix_buffer_count);
+    command_list_header->sample_count = sample_count;
+    command_list_header->sample_rate = sample_rate;
+    command_list_header->samples_buffer = samples_workbuffer;
+
+    const auto performance_initialized{performance_manager.IsInitialized()};
+    if (performance_initialized) {
+        performance_manager.TapFrame(adsp_behind, num_voices_dropped, render_start_tick);
+        adsp_behind = false;
+        num_voices_dropped = 0;
+        render_start_tick = 0;
+    }
+
+    s8 channel_count{2};
+    if (execution_mode == ExecutionMode::Auto) {
+        const auto& sink{core.AudioCore().GetOutputSink()};
+        channel_count = static_cast<s8>(sink.GetDeviceChannels());
+    }
+
+    AudioRendererSystemContext render_context{
+        .session_id{session_id},
+        .channels{channel_count},
+        .mix_buffer_count{mix_buffer_count},
+        .behavior{&behavior},
+        .depop_buffer{depop_buffer},
+        .upsampler_manager{upsampler_manager},
+        .memory_pool_info{&memory_pool_info},
+    };
+
+    CommandBuffer command_buffer{
+        .command_list{in_command_buffer},
+        .sample_count{sample_count},
+        .sample_rate{sample_rate},
+        .size{sizeof(CommandListHeader)},
+        .count{0},
+        .estimated_process_time{0},
+        .memory_pool{&memory_pool_info},
+        .time_estimator{command_processing_time_estimator.get()},
+        .behavior{&behavior},
+    };
+
+    PerformanceManager* perf_manager{nullptr};
+    if (performance_initialized) {
+        perf_manager = &performance_manager;
+    }
+
+    CommandGenerator command_generator{command_buffer, *command_list_header, render_context,
+                                       voice_context,  mix_context,          effect_context,
+                                       sink_context,   splitter_context,     perf_manager};
+
+    voice_context.SortInfo();
+
+    const auto start_estimated_time{command_buffer.estimated_process_time};
+
+    command_generator.GenerateVoiceCommands();
+    command_generator.GenerateSubMixCommands();
+    command_generator.GenerateFinalMixCommands();
+    command_generator.GenerateSinkCommands();
+
+    if (drop_voice) {
+        f32 time_limit_percent{70.0f};
+        if (render_context.behavior->IsAudioRendererProcessingTimeLimit80PercentSupported()) {
+            time_limit_percent = 80.0f;
+        } else if (render_context.behavior
+                       ->IsAudioRendererProcessingTimeLimit75PercentSupported()) {
+            time_limit_percent = 75.0f;
+        } else {
+            // result is ignored
+            render_context.behavior->IsAudioRendererProcessingTimeLimit70PercentSupported();
+            time_limit_percent = 70.0f;
+        }
+        const auto time_limit{static_cast<u32>(
+            static_cast<f32>(start_estimated_time - command_buffer.estimated_process_time) +
+            (((time_limit_percent / 100.0f) * 2'880'000.0) *
+             (static_cast<f32>(render_time_limit_percent) / 100.0f)))};
+        num_voices_dropped = DropVoices(command_buffer, start_estimated_time, time_limit);
+    }
+
+    command_list_header->buffer_size = command_buffer.size;
+    command_list_header->command_count = command_buffer.count;
+
+    voice_context.UpdateStateByDspShared();
+
+    if (render_context.behavior->IsEffectInfoVersion2Supported()) {
+        effect_context.UpdateStateByDspShared();
+    }
+
+    const auto end_time{core.CoreTiming().GetClockTicks()};
+    total_ticks_elapsed += end_time - start_time;
+    num_command_lists_generated++;
+    render_start_tick = adsp.GetRenderingStartTick(session_id);
+    frames_elapsed++;
+
+    return command_buffer.size;
+}
+
+u32 System::DropVoices(CommandBuffer& command_buffer, const u32 estimated_process_time,
+                       const u32 time_limit) {
+    u32 i{0};
+    auto command_list{command_buffer.command_list.data() + sizeof(CommandListHeader)};
+    ICommand* cmd{};
+
+    for (; i < command_buffer.count; i++) {
+        cmd = reinterpret_cast<ICommand*>(command_list);
+        if (cmd->type != CommandId::Performance &&
+            cmd->type != CommandId::DataSourcePcmInt16Version1 &&
+            cmd->type != CommandId::DataSourcePcmInt16Version2 &&
+            cmd->type != CommandId::DataSourcePcmFloatVersion1 &&
+            cmd->type != CommandId::DataSourcePcmFloatVersion2 &&
+            cmd->type != CommandId::DataSourceAdpcmVersion1 &&
+            cmd->type != CommandId::DataSourceAdpcmVersion2) {
+            break;
+        }
+        command_list += cmd->size;
+    }
+
+    if (cmd == nullptr || command_buffer.count == 0 || i >= command_buffer.count) {
+        return 0;
+    }
+
+    auto voices_dropped{0};
+    while (i < command_buffer.count) {
+        const auto node_id{cmd->node_id};
+        const auto node_id_type{cmd->node_id >> 28};
+        const auto node_id_base{cmd->node_id & 0xFFF};
+
+        if (estimated_process_time <= time_limit) {
+            break;
+        }
+
+        if (node_id_type != 1) {
+            break;
+        }
+
+        auto& voice_info{voice_context.GetInfo(node_id_base)};
+        if (voice_info.priority == HighestVoicePriority) {
+            break;
+        }
+
+        voices_dropped++;
+        voice_info.voice_dropped = true;
+
+        if (i < command_buffer.count) {
+            while (cmd->node_id == node_id) {
+                if (cmd->type == CommandId::DepopPrepare) {
+                    cmd->enabled = true;
+                } else if (cmd->type == CommandId::Performance || !cmd->enabled) {
+                    cmd->enabled = false;
+                }
+                i++;
+                command_list += cmd->size;
+                cmd = reinterpret_cast<ICommand*>(command_list);
+            }
+        }
+    }
+    return voices_dropped;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/system.h b/src/audio_core/renderer/system.h
new file mode 100644
index 0000000000..bcbe65b070
--- /dev/null
+++ b/src/audio_core/renderer/system.h
@@ -0,0 +1,307 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <memory>
+#include <mutex>
+#include <span>
+
+#include "audio_core/renderer/behavior/behavior_info.h"
+#include "audio_core/renderer/command/command_processing_time_estimator.h"
+#include "audio_core/renderer/effect/effect_context.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/sink_context.h"
+#include "audio_core/renderer/splitter/splitter_context.h"
+#include "audio_core/renderer/upsampler/upsampler_manager.h"
+#include "audio_core/renderer/voice/voice_context.h"
+#include "common/thread.h"
+#include "core/hle/service/audio/errors.h"
+
+namespace Core {
+namespace Memory {
+class Memory;
+}
+class System;
+} // namespace Core
+
+namespace Kernel {
+class KEvent;
+class KTransferMemory;
+} // namespace Kernel
+
+namespace AudioCore {
+struct AudioRendererParameterInternal;
+
+namespace AudioRenderer {
+class CommandBuffer;
+namespace ADSP {
+class ADSP;
+}
+
+/**
+ * Audio Renderer System, the main worker for audio rendering.
+ */
+class System {
+    enum class State {
+        Started = 0,
+        Stopped = 2,
+    };
+
+public:
+    explicit System(Core::System& core, Kernel::KEvent* adsp_rendered_event);
+
+    /**
+     * Calculate the total size required for all audio render workbuffers.
+     *
+     * @param params - Input parameters with the numbers of voices/mixes/sinks/etc.
+     * @return Size (in bytes) required for the audio renderer.
+     */
+    static u64 GetWorkBufferSize(const AudioRendererParameterInternal& params);
+
+    /**
+     * Initialize the renderer system.
+     * Allocates workbuffers and initializes everything to a default state, ready to receive a
+     * RequestUpdate.
+     *
+     * @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. Unused.
+     * @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,
+                      u32 process_handle, u64 applet_resource_user_id, s32 session_id);
+
+    /**
+     * Finalize the system.
+     */
+    void Finalize();
+
+    /**
+     * Start the system.
+     */
+    void Start();
+
+    /**
+     * Stop the system.
+     */
+    void Stop();
+
+    /**
+     * Update the system.
+     *
+     * @param input       - Inout buffer containing the update data.
+     * @param performance - Optional buffer for writing back performance metrics.
+     * @param output      - Output information from rendering.
+     * @return Result code.
+     */
+    Result Update(std::span<const u8> input, std::span<u8> performance, std::span<u8> output);
+
+    /**
+     * Get the time limit (percent) for rendering
+     *
+     * @return Time limit as a percent.
+     */
+    u32 GetRenderingTimeLimit() const;
+
+    /**
+     * Set the time limit (percent) for rendering
+     *
+     * @param limit - New time limit.
+     */
+    void SetRenderingTimeLimit(u32 limit);
+
+    /**
+     * Get the session id for this system.
+     *
+     * @return Session id of this system.
+     */
+    u32 GetSessionId() const;
+
+    /**
+     * Get the sample rate of this system.
+     *
+     * @return Sample rate of this system.
+     */
+    u32 GetSampleRate() const;
+
+    /**
+     * Get the sample count of this system.
+     *
+     * @return Sample count of this system.
+     */
+    u32 GetSampleCount() const;
+
+    /**
+     * Get the number of mix buffers for this system.
+     *
+     * @return Number of mix buffers in the system.
+     */
+    u32 GetMixBufferCount() const;
+
+    /**
+     * Get the execution mode of this system.
+     * Note: Only Auto is implemented.
+     *
+     * @return Execution mode for this system.
+     */
+    ExecutionMode GetExecutionMode() const;
+
+    /**
+     * Get the rendering deivce for this system.
+     * This is unused.
+     *
+     * @return Rendering device for this system.
+     */
+    u32 GetRenderingDevice() const;
+
+    /**
+     * Check if this system is currently active.
+     *
+     * @return True if active, otherwise false.
+     */
+    bool IsActive() const;
+
+    /**
+     * Prepare and generate a list of commands for the AudioRenderer based on current state,
+     * signalling the buffer event when all processed.
+     */
+    void SendCommandToDsp();
+
+    /**
+     * Generate a list of commands for the AudioRenderer based on current state.
+     *
+     * @param command_buffer      - Buffer for commands to be written to.
+     * @param command_buffer_size - Size of the command_buffer.
+     *
+     * @return Number of bytes written.
+     */
+    u64 GenerateCommand(std::span<u8> command_buffer, u64 command_buffer_size);
+
+    /**
+     * Try to drop some voices if the AudioRenderer fell behind.
+     *
+     * @param command_buffer         - Command buffer to drop voices from.
+     * @param estimated_process_time - Current estimated processing time of all commands.
+     * @param time_limit             - Time limit for rendering, voices are dropped if estimated
+     *                                 exceeds this.
+     *
+     * @return Number of voices dropped.
+     */
+    u32 DropVoices(CommandBuffer& command_buffer, u32 estimated_process_time, u32 time_limit);
+
+private:
+    /// Core system
+    Core::System& core;
+    /// Reference to the ADSP for communication
+    ADSP::ADSP& adsp;
+    /// Is this system initialized?
+    bool initialized{};
+    /// Is this system currently active?
+    std::atomic<bool> active{};
+    /// State of the system
+    State state{State::Stopped};
+    /// Sample rate for the system
+    u32 sample_rate{};
+    /// Sample count of the system
+    u32 sample_count{};
+    /// Number of mix buffers in use by the system
+    s16 mix_buffer_count{};
+    /// Workbuffer for mix buffers, used by the AudioRenderer
+    std::span<s32> samples_workbuffer{};
+    /// Depop samples for depopping commands
+    std::span<s32> depop_buffer{};
+    /// Number of memory pools in the buffer
+    u32 memory_pool_count{};
+    /// Workbuffer for memory pools
+    std::span<MemoryPoolInfo> memory_pool_workbuffer{};
+    /// System memory pool info
+    MemoryPoolInfo memory_pool_info{};
+    /// Workbuffer that commands will be generated into
+    std::span<u8> command_workbuffer{};
+    /// Size of command workbuffer
+    u64 command_workbuffer_size{};
+    /// Numebr of commands in the workbuffer
+    u64 command_buffer_size{};
+    /// Manager for upsamplers
+    UpsamplerManager* upsampler_manager{};
+    /// Upsampler workbuffer
+    std::span<UpsamplerInfo> upsampler_infos{};
+    /// Number of upsamplers in the workbuffer
+    u32 upsampler_count{};
+    /// Holds and controls all voices
+    VoiceContext voice_context{};
+    /// Holds and controls all mixes
+    MixContext mix_context{};
+    /// Holds and controls all effects
+    EffectContext effect_context{};
+    /// Holds and controls all sinks
+    SinkContext sink_context{};
+    /// Holds and controls all splitters
+    SplitterContext splitter_context{};
+    /// Estimates the time taken for each command
+    std::unique_ptr<ICommandProcessingTimeEstimator> command_processing_time_estimator{};
+    /// Session id of this system
+    s32 session_id{};
+    /// Number of channels in use by voices
+    s32 voice_channels{};
+    /// Event to be called when the AudioRenderer processes a command list
+    Kernel::KEvent* adsp_rendered_event{};
+    /// Event signalled on system terminate
+    Common::Event terminate_event{};
+    /// Does what locks do
+    std::mutex lock{};
+    /// Handle for the process for this system, unused
+    u32 process_handle{};
+    /// Applet resource id for this system, unused
+    u64 applet_resource_user_id{};
+    /// Controls performance input and output
+    PerformanceManager performance_manager{};
+    /// Workbuffer for performance metrics
+    std::span<u8> performance_workbuffer{};
+    /// Main workbuffer, from which all other workbuffers here allocate into
+    std::unique_ptr<u8[]> workbuffer{};
+    /// Size of the main workbuffer
+    u64 workbuffer_size{};
+    /// Unknown buffer/marker
+    std::span<u8> unk_2A8{};
+    /// Size of the above unknown buffer/marker
+    u64 unk_2B0{};
+    /// Rendering time limit (percent)
+    u32 render_time_limit_percent{};
+    /// Should any voices be dropped?
+    bool drop_voice{};
+    /// Should the backend stream have its buffers flushed?
+    bool reset_command_buffers{};
+    /// Execution mode of this system, only Auto is supported
+    ExecutionMode execution_mode{ExecutionMode::Auto};
+    /// Render device, unused
+    u32 render_device{};
+    /// Behaviour to check which features are supported by the user revision
+    BehaviorInfo behavior{};
+    /// Total ticks the audio system has been running
+    u64 total_ticks_elapsed{};
+    /// Ticks the system has spent in updates
+    u64 ticks_spent_updating{};
+    /// Number of times a command list was generated
+    u64 num_command_lists_generated{};
+    /// Number of times the system has updated
+    u64 num_times_updated{};
+    /// Number of frames generated, written back to the game
+    std::atomic<u64> frames_elapsed{};
+    /// Is the AudioRenderer running too slow?
+    bool adsp_behind{};
+    /// Number of voices dropped
+    u32 num_voices_dropped{};
+    /// Tick that rendering started
+    u64 render_start_tick{};
+};
+
+} // namespace AudioRenderer
+} // namespace AudioCore
diff --git a/src/audio_core/renderer/system_manager.cpp b/src/audio_core/renderer/system_manager.cpp
new file mode 100644
index 0000000000..b326819eda
--- /dev/null
+++ b/src/audio_core/renderer/system_manager.cpp
@@ -0,0 +1,162 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <chrono>
+
+#include "audio_core/audio_core.h"
+#include "audio_core/renderer/adsp/adsp.h"
+#include "audio_core/renderer/system_manager.h"
+#include "common/microprofile.h"
+#include "common/thread.h"
+#include "core/core.h"
+#include "core/core_timing.h"
+
+MICROPROFILE_DEFINE(Audio_RenderSystemManager, "Audio", "Render System Manager",
+                    MP_RGB(60, 19, 97));
+
+namespace AudioCore::AudioRenderer {
+constexpr std::chrono::nanoseconds BaseRenderTime{5'000'000UL};
+constexpr std::chrono::nanoseconds RenderTimeOffset{400'000UL};
+
+SystemManager::SystemManager(Core::System& core_)
+    : core{core_}, adsp{core.AudioCore().GetADSP()}, mailbox{adsp.GetRenderMailbox()},
+      thread_event{Core::Timing::CreateEvent(
+          "AudioRendererSystemManager", [this](std::uintptr_t, s64 time, std::chrono::nanoseconds) {
+              return ThreadFunc2(time);
+          })} {
+    core.CoreTiming().RegisterPauseCallback([this](bool paused) { PauseCallback(paused); });
+}
+
+SystemManager::~SystemManager() {
+    Stop();
+}
+
+bool SystemManager::InitializeUnsafe() {
+    if (!active) {
+        if (adsp.Start()) {
+            active = true;
+            thread = std::jthread([this](std::stop_token stop_token) { ThreadFunc(); });
+            core.CoreTiming().ScheduleLoopingEvent(std::chrono::nanoseconds(0),
+                                                   BaseRenderTime - RenderTimeOffset, thread_event);
+        }
+    }
+
+    return adsp.GetState() == ADSP::State::Started;
+}
+
+void SystemManager::Stop() {
+    if (!active) {
+        return;
+    }
+    core.CoreTiming().UnscheduleEvent(thread_event, {});
+    active = false;
+    update.store(true);
+    update.notify_all();
+    thread.join();
+    adsp.Stop();
+}
+
+bool SystemManager::Add(System& system_) {
+    std::scoped_lock l2{mutex2};
+
+    if (systems.size() + 1 > MaxRendererSessions) {
+        LOG_ERROR(Service_Audio, "Maximum AudioRenderer Systems active, cannot add more!");
+        return false;
+    }
+
+    {
+        std::scoped_lock l{mutex1};
+        if (systems.empty()) {
+            if (!InitializeUnsafe()) {
+                LOG_ERROR(Service_Audio, "Failed to start the AudioRenderer SystemManager");
+                return false;
+            }
+        }
+    }
+
+    systems.push_back(&system_);
+    return true;
+}
+
+bool SystemManager::Remove(System& system_) {
+    std::scoped_lock l2{mutex2};
+
+    {
+        std::scoped_lock l{mutex1};
+        if (systems.remove(&system_) == 0) {
+            LOG_ERROR(Service_Audio,
+                      "Failed to remove a render system, it was not found in the list!");
+            return false;
+        }
+    }
+
+    if (systems.empty()) {
+        Stop();
+    }
+    return true;
+}
+
+void SystemManager::ThreadFunc() {
+    constexpr char name[]{"yuzu:AudioRenderSystemManager"};
+    MicroProfileOnThreadCreate(name);
+    Common::SetCurrentThreadName(name);
+    Common::SetCurrentThreadPriority(Common::ThreadPriority::High);
+    while (active) {
+        {
+            std::scoped_lock l{mutex1};
+
+            MICROPROFILE_SCOPE(Audio_RenderSystemManager);
+
+            for (auto system : systems) {
+                system->SendCommandToDsp();
+            }
+        }
+
+        adsp.Signal();
+        adsp.Wait();
+
+        update.wait(false);
+        update.store(false);
+    }
+}
+
+std::optional<std::chrono::nanoseconds> SystemManager::ThreadFunc2(s64 time) {
+    std::optional<std::chrono::nanoseconds> new_schedule_time{std::nullopt};
+    const auto queue_size{core.AudioCore().GetStreamQueue()};
+    switch (state) {
+    case StreamState::Filling:
+        if (queue_size >= 5) {
+            new_schedule_time = BaseRenderTime;
+            state = StreamState::Steady;
+        }
+        break;
+    case StreamState::Steady:
+        if (queue_size <= 2) {
+            new_schedule_time = BaseRenderTime - RenderTimeOffset;
+            state = StreamState::Filling;
+        } else if (queue_size > 5) {
+            new_schedule_time = BaseRenderTime + RenderTimeOffset;
+            state = StreamState::Draining;
+        }
+        break;
+    case StreamState::Draining:
+        if (queue_size <= 5) {
+            new_schedule_time = BaseRenderTime;
+            state = StreamState::Steady;
+        }
+        break;
+    }
+
+    update.store(true);
+    update.notify_all();
+    return new_schedule_time;
+}
+
+void SystemManager::PauseCallback(bool paused) {
+    if (paused && core.IsPoweredOn() && core.IsShuttingDown()) {
+        update.store(true);
+        update.notify_all();
+    }
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/system_manager.h b/src/audio_core/renderer/system_manager.h
new file mode 100644
index 0000000000..1291e9e0ef
--- /dev/null
+++ b/src/audio_core/renderer/system_manager.h
@@ -0,0 +1,113 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <list>
+#include <memory>
+#include <mutex>
+#include <optional>
+#include <thread>
+
+#include "audio_core/renderer/system.h"
+
+namespace Core {
+namespace Timing {
+struct EventType;
+}
+class System;
+} // namespace Core
+
+namespace AudioCore::AudioRenderer {
+namespace ADSP {
+class ADSP;
+class AudioRenderer_Mailbox;
+} // namespace ADSP
+
+/**
+ * Manages all audio renderers, responsible for triggering command list generation and signalling
+ * the ADSP.
+ */
+class SystemManager {
+public:
+    explicit SystemManager(Core::System& core);
+    ~SystemManager();
+
+    /**
+     * Initialize the system manager, called when any system is registered.
+     *
+     * @return True if sucessfully initialized, otherwise false.
+     */
+    bool InitializeUnsafe();
+
+    /**
+     * Stop the system manager.
+     */
+    void Stop();
+
+    /**
+     * Add an audio render system to the manager.
+     * The manager does not own the system, so do not free it without calling Remove.
+     *
+     * @param system - The system to add.
+     * @return True if succesfully added, otherwise false.
+     */
+    bool Add(System& system);
+
+    /**
+     * Remove an audio render system from the manager.
+     *
+     * @param system - The system to remove.
+     * @return True if succesfully removed, otherwise false.
+     */
+    bool Remove(System& system);
+
+private:
+    /**
+     * Main thread responsible for command generation.
+     */
+    void ThreadFunc();
+
+    /**
+     * Signalling core timing thread to run ThreadFunc.
+     */
+    std::optional<std::chrono::nanoseconds> ThreadFunc2(s64 time);
+
+    /**
+     * Callback from core timing when pausing, used to detect shutdowns and stop ThreadFunc.
+     *
+     * @param paused - Are we pausing or resuming?
+     */
+    void PauseCallback(bool paused);
+
+    enum class StreamState {
+        Filling,
+        Steady,
+        Draining,
+    };
+
+    /// Core system
+    Core::System& core;
+    /// List of pointers to managed systems
+    std::list<System*> systems{};
+    /// Main worker thread for generating command lists
+    std::jthread thread;
+    /// Mutex for the systems
+    std::mutex mutex1{};
+    /// Mutex for adding/removing systems
+    std::mutex mutex2{};
+    /// Is the system manager thread active?
+    std::atomic<bool> active{};
+    /// Reference to the ADSP for communication
+    ADSP::ADSP& adsp;
+    /// AudioRenderer mailbox for communication
+    ADSP::AudioRenderer_Mailbox* mailbox{};
+    /// Core timing event to signal main thread
+    std::shared_ptr<Core::Timing::EventType> thread_event;
+    /// Atomic for main thread to wait on
+    std::atomic<bool> update{};
+    /// Current state of the streams
+    StreamState state{StreamState::Filling};
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/upsampler/upsampler_info.cpp b/src/audio_core/renderer/upsampler/upsampler_info.cpp
new file mode 100644
index 0000000000..e3d2f7db06
--- /dev/null
+++ b/src/audio_core/renderer/upsampler/upsampler_info.cpp
@@ -0,0 +1,6 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/upsampler/upsampler_info.h"
+
+namespace AudioCore::AudioRenderer {} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/upsampler/upsampler_info.h b/src/audio_core/renderer/upsampler/upsampler_info.h
new file mode 100644
index 0000000000..a43c15af31
--- /dev/null
+++ b/src/audio_core/renderer/upsampler/upsampler_info.h
@@ -0,0 +1,35 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+
+#include "audio_core/common/common.h"
+#include "audio_core/renderer/upsampler/upsampler_state.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+class UpsamplerManager;
+
+/**
+ * Manages information needed to upsample a mix buffer.
+ */
+struct UpsamplerInfo {
+    /// States used by the AudioRenderer across calls.
+    std::array<UpsamplerState, MaxChannels> states{};
+    /// Pointer to the manager
+    UpsamplerManager* manager{};
+    /// Pointer to the samples to be upsampled
+    CpuAddr samples_pos{};
+    /// Target number of samples to upsample to
+    u32 sample_count{};
+    /// Number of channels to upsample
+    u32 input_count{};
+    /// Is this upsampler enabled?
+    bool enabled{};
+    /// Mix buffer indexes to be upsampled
+    std::array<s16, MaxChannels> inputs{};
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/upsampler/upsampler_manager.cpp b/src/audio_core/renderer/upsampler/upsampler_manager.cpp
new file mode 100644
index 0000000000..4c76a50660
--- /dev/null
+++ b/src/audio_core/renderer/upsampler/upsampler_manager.cpp
@@ -0,0 +1,44 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/upsampler/upsampler_manager.h"
+
+namespace AudioCore::AudioRenderer {
+
+UpsamplerManager::UpsamplerManager(const u32 count_, std::span<UpsamplerInfo> infos_,
+                                   std::span<s32> workbuffer_)
+    : count{count_}, upsampler_infos{infos_}, workbuffer{workbuffer_} {}
+
+UpsamplerInfo* UpsamplerManager::Allocate() {
+    std::scoped_lock l{lock};
+
+    if (count == 0) {
+        return nullptr;
+    }
+
+    u32 free_index{0};
+    for (auto& upsampler : upsampler_infos) {
+        if (!upsampler.enabled) {
+            break;
+        }
+        free_index++;
+    }
+
+    if (free_index >= count) {
+        return nullptr;
+    }
+
+    auto& upsampler{upsampler_infos[free_index]};
+    upsampler.manager = this;
+    upsampler.sample_count = TargetSampleCount;
+    upsampler.samples_pos = CpuAddr(&workbuffer[upsampler.sample_count * MaxChannels]);
+    upsampler.enabled = true;
+    return &upsampler;
+}
+
+void UpsamplerManager::Free(UpsamplerInfo* info) {
+    std::scoped_lock l{lock};
+    info->enabled = false;
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/upsampler/upsampler_manager.h b/src/audio_core/renderer/upsampler/upsampler_manager.h
new file mode 100644
index 0000000000..70cd42b08d
--- /dev/null
+++ b/src/audio_core/renderer/upsampler/upsampler_manager.h
@@ -0,0 +1,45 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <mutex>
+#include <span>
+
+#include "audio_core/renderer/upsampler/upsampler_info.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * Manages and has utility functions for upsampler infos.
+ */
+class UpsamplerManager {
+public:
+    UpsamplerManager(u32 count, std::span<UpsamplerInfo> infos, std::span<s32> workbuffer);
+
+    /**
+     * Allocate a new UpsamplerInfo.
+     *
+     * @return The allocated upsampler, may be nullptr if alloc failed.
+     */
+    UpsamplerInfo* Allocate();
+
+    /**
+     * Free the given upsampler.
+     *
+     * @param The upsampler to be freed.
+     */
+    void Free(UpsamplerInfo* info);
+
+private:
+    /// Maximum number of upsamplers in the buffer
+    const u32 count;
+    /// Upsamplers buffer
+    std::span<UpsamplerInfo> upsampler_infos;
+    /// Workbuffer for upsampling samples
+    std::span<s32> workbuffer;
+    /// Lock for allocate/free
+    std::mutex lock{};
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/upsampler/upsampler_state.h b/src/audio_core/renderer/upsampler/upsampler_state.h
new file mode 100644
index 0000000000..28cebe2004
--- /dev/null
+++ b/src/audio_core/renderer/upsampler/upsampler_state.h
@@ -0,0 +1,40 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+
+#include "common/common_types.h"
+#include "common/fixed_point.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * Upsampling state used by the AudioRenderer across calls.
+ */
+struct UpsamplerState {
+    static constexpr u16 HistorySize = 20;
+
+    /// Source data to target data ratio. E.g 48'000/32'000 = 1.5
+    Common::FixedPoint<16, 16> ratio;
+    /// Sample history
+    std::array<Common::FixedPoint<24, 8>, HistorySize> history;
+    /// Size of the sinc coefficient window
+    u16 window_size;
+    /// Read index for the history
+    u16 history_output_index;
+    /// Write index for the history
+    u16 history_input_index;
+    /// Start offset within the history, fixed to 0
+    u16 history_start_index;
+    /// Ebd offset within the history, fixed to HistorySize
+    u16 history_end_index;
+    /// Is this state initialized?
+    bool initialized;
+    /// Index of the current sample.
+    /// E.g 16K -> 48K has a ratio of 3, so this will be 0-2.
+    /// See the Upsample command in the AudioRenderer for more information.
+    u8 sample_index;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/voice/voice_channel_resource.h b/src/audio_core/renderer/voice/voice_channel_resource.h
new file mode 100644
index 0000000000..26ab4ccceb
--- /dev/null
+++ b/src/audio_core/renderer/voice/voice_channel_resource.h
@@ -0,0 +1,38 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+
+#include "audio_core/common/common.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * Represents one channel for mixing a voice.
+ */
+class VoiceChannelResource {
+public:
+    struct InParameter {
+        /* 0x00 */ u32 id;
+        /* 0x04 */ std::array<f32, MaxMixBuffers> mix_volumes;
+        /* 0x64 */ bool in_use;
+        /* 0x65 */ char unk65[0xB];
+    };
+    static_assert(sizeof(InParameter) == 0x70,
+                  "VoiceChannelResource::InParameter has the wrong size!");
+
+    explicit VoiceChannelResource(u32 id_) : id{id_} {}
+
+    /// Current volume for each mix buffer
+    std::array<f32, MaxMixBuffers> mix_volumes{};
+    /// Previous volume for each mix buffer
+    std::array<f32, MaxMixBuffers> prev_mix_volumes{};
+    /// Id of this resource
+    const u32 id;
+    /// Is this resource in use?
+    bool in_use{};
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/voice/voice_context.cpp b/src/audio_core/renderer/voice/voice_context.cpp
new file mode 100644
index 0000000000..eafb51b011
--- /dev/null
+++ b/src/audio_core/renderer/voice/voice_context.cpp
@@ -0,0 +1,86 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <ranges>
+
+#include "audio_core/renderer/voice/voice_context.h"
+
+namespace AudioCore::AudioRenderer {
+
+VoiceState& VoiceContext::GetDspSharedState(const u32 index) {
+    if (index >= dsp_states.size()) {
+        LOG_ERROR(Service_Audio, "Invalid voice dsp state index {:04X}", index);
+    }
+    return dsp_states[index];
+}
+
+VoiceChannelResource& VoiceContext::GetChannelResource(const u32 index) {
+    if (index >= channel_resources.size()) {
+        LOG_ERROR(Service_Audio, "Invalid voice channel resource index {:04X}", index);
+    }
+    return channel_resources[index];
+}
+
+void VoiceContext::Initialize(std::span<VoiceInfo*> sorted_voice_infos_,
+                              std::span<VoiceInfo> voice_infos_,
+                              std::span<VoiceChannelResource> voice_channel_resources_,
+                              std::span<VoiceState> cpu_states_, std::span<VoiceState> dsp_states_,
+                              const u32 voice_count_) {
+    sorted_voice_info = sorted_voice_infos_;
+    voices = voice_infos_;
+    channel_resources = voice_channel_resources_;
+    cpu_states = cpu_states_;
+    dsp_states = dsp_states_;
+    voice_count = voice_count_;
+    active_count = 0;
+}
+
+VoiceInfo* VoiceContext::GetSortedInfo(const u32 index) {
+    if (index >= sorted_voice_info.size()) {
+        LOG_ERROR(Service_Audio, "Invalid voice sorted info index {:04X}", index);
+    }
+    return sorted_voice_info[index];
+}
+
+VoiceInfo& VoiceContext::GetInfo(const u32 index) {
+    if (index >= voices.size()) {
+        LOG_ERROR(Service_Audio, "Invalid voice info index {:04X}", index);
+    }
+    return voices[index];
+}
+
+VoiceState& VoiceContext::GetState(const u32 index) {
+    if (index >= cpu_states.size()) {
+        LOG_ERROR(Service_Audio, "Invalid voice cpu state index {:04X}", index);
+    }
+    return cpu_states[index];
+}
+
+u32 VoiceContext::GetCount() const {
+    return voice_count;
+}
+
+u32 VoiceContext::GetActiveCount() const {
+    return active_count;
+}
+
+void VoiceContext::SetActiveCount(const u32 active_count_) {
+    active_count = active_count_;
+}
+
+void VoiceContext::SortInfo() {
+    for (u32 i = 0; i < voice_count; i++) {
+        sorted_voice_info[i] = &voices[i];
+    }
+
+    std::ranges::sort(sorted_voice_info, [](const VoiceInfo* a, const VoiceInfo* b) {
+        return a->priority != b->priority ? a->priority < b->priority
+                                          : a->sort_order < b->sort_order;
+    });
+}
+
+void VoiceContext::UpdateStateByDspShared() {
+    std::memcpy(cpu_states.data(), dsp_states.data(), voice_count * sizeof(VoiceState));
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/voice/voice_context.h b/src/audio_core/renderer/voice/voice_context.h
new file mode 100644
index 0000000000..43b6771547
--- /dev/null
+++ b/src/audio_core/renderer/voice/voice_context.h
@@ -0,0 +1,126 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <span>
+
+#include "audio_core/renderer/voice/voice_channel_resource.h"
+#include "audio_core/renderer/voice/voice_info.h"
+#include "audio_core/renderer/voice/voice_state.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * Contains all voices, with utility functions for managing them.
+ */
+class VoiceContext {
+public:
+    /**
+     * Get the AudioRenderer state for a given index
+     *
+     * @param index - State index to get.
+     * @return The requested voice state.
+     */
+    VoiceState& GetDspSharedState(u32 index);
+
+    /**
+     * Get the channel resource for a given index
+     *
+     * @param index - Resource index to get.
+     * @return The requested voice resource.
+     */
+    VoiceChannelResource& GetChannelResource(u32 index);
+
+    /**
+     * Initialize the voice context.
+     *
+     * @param sorted_voice_infos      - Workbuffer for the sorted voices.
+     * @param voice_infos             - Workbuffer for the voices.
+     * @param voice_channel_resources - Workbuffer for the voice channel resources.
+     * @param cpu_states              - Workbuffer for the host-side voice states.
+     * @param dsp_states              - Workbuffer for the AudioRenderer-side voice states.
+     * @param voice_count             - The number of voices in each workbuffer.
+     */
+    void Initialize(std::span<VoiceInfo*> sorted_voice_infos, std::span<VoiceInfo> voice_infos,
+                    std::span<VoiceChannelResource> voice_channel_resources,
+                    std::span<VoiceState> cpu_states, std::span<VoiceState> dsp_states,
+                    u32 voice_count);
+
+    /**
+     * Get a sorted voice with the given index.
+     *
+     * @param index - The sorted voice index to get.
+     * @return The sorted voice.
+     */
+    VoiceInfo* GetSortedInfo(u32 index);
+
+    /**
+     * Get a voice with the given index.
+     *
+     * @param index - The voice index to get.
+     * @return The voice.
+     */
+    VoiceInfo& GetInfo(u32 index);
+
+    /**
+     * Get a host voice state with the given index.
+     *
+     * @param index - The host voice state index to get.
+     * @return The voice state.
+     */
+    VoiceState& GetState(u32 index);
+
+    /**
+     * Get the maximum number of voices.
+     * Not all voices in the buffers may be in use, see GetActiveCount.
+     *
+     * @return The maximum number of voices.
+     */
+    u32 GetCount() const;
+
+    /**
+     * Get the number of active voices.
+     * Can be less than or equal to the maximum number of voices.
+     *
+     * @return The number of active voices.
+     */
+    u32 GetActiveCount() const;
+
+    /**
+     * Set the number of active voices.
+     * Can be less than or equal to the maximum number of voices.
+     *
+     * @param active_count - The new number of active voices.
+     */
+    void SetActiveCount(u32 active_count);
+
+    /**
+     * Sort all voices. Results are available via GetSortedInfo.
+     * Voices are sorted descendingly, according to priority, and then sort order.
+     */
+    void SortInfo();
+
+    /**
+     * Update all voice states, copying AudioRenderer-side states to host-side states.
+     */
+    void UpdateStateByDspShared();
+
+private:
+    /// Sorted voices
+    std::span<VoiceInfo*> sorted_voice_info{};
+    /// Voices
+    std::span<VoiceInfo> voices{};
+    /// Channel resources
+    std::span<VoiceChannelResource> channel_resources{};
+    /// Host-side voice states
+    std::span<VoiceState> cpu_states{};
+    /// AudioRenderer-side voice states
+    std::span<VoiceState> dsp_states{};
+    /// Maximum number of voices
+    u32 voice_count{};
+    /// Number of active voices
+    u32 active_count{};
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/voice/voice_info.cpp b/src/audio_core/renderer/voice/voice_info.cpp
new file mode 100644
index 0000000000..1849eeb579
--- /dev/null
+++ b/src/audio_core/renderer/voice/voice_info.cpp
@@ -0,0 +1,408 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/renderer/memory/pool_mapper.h"
+#include "audio_core/renderer/voice/voice_context.h"
+#include "audio_core/renderer/voice/voice_info.h"
+#include "audio_core/renderer/voice/voice_state.h"
+
+namespace AudioCore::AudioRenderer {
+
+VoiceInfo::VoiceInfo() {
+    Initialize();
+}
+
+void VoiceInfo::Initialize() {
+    in_use = false;
+    is_new = false;
+    id = 0;
+    node_id = 0;
+    current_play_state = ServerPlayState::Stopped;
+    src_quality = SrcQuality::Medium;
+    priority = LowestVoicePriority;
+    sample_format = SampleFormat::Invalid;
+    sample_rate = 0;
+    channel_count = 0;
+    wave_buffer_count = 0;
+    wave_buffer_index = 0;
+    pitch = 0.0f;
+    volume = 0.0f;
+    prev_volume = 0.0f;
+    mix_id = UnusedMixId;
+    splitter_id = UnusedSplitterId;
+    biquads = {};
+    biquad_initialized = {};
+    voice_dropped = false;
+    data_unmapped = false;
+    buffer_unmapped = false;
+    flush_buffer_count = 0;
+
+    data_address.Setup(0, 0);
+    for (auto& wavebuffer : wavebuffers) {
+        wavebuffer.Initialize();
+    }
+}
+
+bool VoiceInfo::ShouldUpdateParameters(const InParameter& params) const {
+    return data_address.GetCpuAddr() != params.src_data_address ||
+           data_address.GetSize() != params.src_data_size || data_unmapped;
+}
+
+void VoiceInfo::UpdateParameters(BehaviorInfo::ErrorInfo& error_info, const InParameter& params,
+                                 const PoolMapper& pool_mapper, const BehaviorInfo& behavior) {
+    in_use = params.in_use;
+    id = params.id;
+    node_id = params.node_id;
+    UpdatePlayState(params.play_state);
+    UpdateSrcQuality(params.src_quality);
+    priority = params.priority;
+    sort_order = params.sort_order;
+    sample_rate = params.sample_rate;
+    sample_format = params.sample_format;
+    channel_count = static_cast<s8>(params.channel_count);
+    pitch = params.pitch;
+    volume = params.volume;
+    biquads = params.biquads;
+    wave_buffer_count = params.wave_buffer_count;
+    wave_buffer_index = params.wave_buffer_index;
+
+    if (behavior.IsFlushVoiceWaveBuffersSupported()) {
+        flush_buffer_count += params.flush_buffer_count;
+    }
+
+    mix_id = params.mix_id;
+
+    if (behavior.IsSplitterSupported()) {
+        splitter_id = params.splitter_id;
+    } else {
+        splitter_id = UnusedSplitterId;
+    }
+
+    channel_resource_ids = params.channel_resource_ids;
+
+    flags &= u16(~0b11);
+    if (behavior.IsVoicePlayedSampleCountResetAtLoopPointSupported()) {
+        flags |= u16(params.flags.IsVoicePlayedSampleCountResetAtLoopPointSupported);
+    }
+
+    if (behavior.IsVoicePitchAndSrcSkippedSupported()) {
+        flags |= u16(params.flags.IsVoicePitchAndSrcSkippedSupported);
+    }
+
+    if (params.clear_voice_drop) {
+        voice_dropped = false;
+    }
+
+    if (ShouldUpdateParameters(params)) {
+        data_unmapped = !pool_mapper.TryAttachBuffer(error_info, data_address,
+                                                     params.src_data_address, params.src_data_size);
+    } else {
+        error_info.error_code = ResultSuccess;
+        error_info.address = CpuAddr(0);
+    }
+}
+
+void VoiceInfo::UpdatePlayState(const PlayState state) {
+    last_play_state = current_play_state;
+
+    switch (state) {
+    case PlayState::Started:
+        current_play_state = ServerPlayState::Started;
+        break;
+    case PlayState::Stopped:
+        if (current_play_state != ServerPlayState::Stopped) {
+            current_play_state = ServerPlayState::RequestStop;
+        }
+        break;
+    case PlayState::Paused:
+        current_play_state = ServerPlayState::Paused;
+        break;
+    default:
+        LOG_ERROR(Service_Audio, "Invalid input play state {}", static_cast<u32>(state));
+        break;
+    }
+}
+
+void VoiceInfo::UpdateSrcQuality(const SrcQuality quality) {
+    switch (quality) {
+    case SrcQuality::Medium:
+        src_quality = quality;
+        break;
+    case SrcQuality::High:
+        src_quality = quality;
+        break;
+    case SrcQuality::Low:
+        src_quality = quality;
+        break;
+    default:
+        LOG_ERROR(Service_Audio, "Invalid input src quality {}", static_cast<u32>(quality));
+        break;
+    }
+}
+
+void VoiceInfo::UpdateWaveBuffers(std::span<std::array<BehaviorInfo::ErrorInfo, 2>> error_infos,
+                                  [[maybe_unused]] u32 error_count, const InParameter& params,
+                                  std::span<VoiceState*> voice_states,
+                                  const PoolMapper& pool_mapper, const BehaviorInfo& behavior) {
+    if (params.is_new) {
+        for (size_t i = 0; i < wavebuffers.size(); i++) {
+            wavebuffers[i].Initialize();
+        }
+
+        for (s8 channel = 0; channel < static_cast<s8>(params.channel_count); channel++) {
+            voice_states[channel]->wave_buffer_valid.fill(false);
+        }
+    }
+
+    for (u32 i = 0; i < MaxWaveBuffers; i++) {
+        UpdateWaveBuffer(error_infos[i], wavebuffers[i], params.wave_buffer_internal[i],
+                         params.sample_format, voice_states[0]->wave_buffer_valid[i], pool_mapper,
+                         behavior);
+    }
+}
+
+void VoiceInfo::UpdateWaveBuffer(std::span<BehaviorInfo::ErrorInfo> error_info,
+                                 WaveBuffer& wave_buffer,
+                                 const WaveBufferInternal& wave_buffer_internal,
+                                 const SampleFormat sample_format_, const bool valid,
+                                 const PoolMapper& pool_mapper, const BehaviorInfo& behavior) {
+    if (!valid && wave_buffer.sent_to_DSP && wave_buffer.buffer_address.GetCpuAddr() != 0) {
+        pool_mapper.ForceUnmapPointer(wave_buffer.buffer_address);
+        wave_buffer.buffer_address.Setup(0, 0);
+    }
+
+    if (!ShouldUpdateWaveBuffer(wave_buffer_internal)) {
+        return;
+    }
+
+    switch (sample_format_) {
+    case SampleFormat::PcmInt16: {
+        constexpr auto byte_size{GetSampleFormatByteSize(SampleFormat::PcmInt16)};
+        if (wave_buffer_internal.start_offset * byte_size > wave_buffer_internal.size ||
+            wave_buffer_internal.end_offset * byte_size > wave_buffer_internal.size) {
+            LOG_ERROR(Service_Audio, "Invalid PCM16 start/end wavebuffer sizes!");
+            error_info[0].error_code = Service::Audio::ERR_INVALID_UPDATE_DATA;
+            error_info[0].address = wave_buffer_internal.address;
+            return;
+        }
+    } break;
+
+    case SampleFormat::PcmFloat: {
+        constexpr auto byte_size{GetSampleFormatByteSize(SampleFormat::PcmFloat)};
+        if (wave_buffer_internal.start_offset * byte_size > wave_buffer_internal.size ||
+            wave_buffer_internal.end_offset * byte_size > wave_buffer_internal.size) {
+            LOG_ERROR(Service_Audio, "Invalid PCMFloat start/end wavebuffer sizes!");
+            error_info[0].error_code = Service::Audio::ERR_INVALID_UPDATE_DATA;
+            error_info[0].address = wave_buffer_internal.address;
+            return;
+        }
+    } break;
+
+    case SampleFormat::Adpcm: {
+        const auto start_frame{wave_buffer_internal.start_offset / 14};
+        auto start_extra{wave_buffer_internal.start_offset % 14 == 0
+                             ? 0
+                             : (wave_buffer_internal.start_offset % 14) / 2 + 1 +
+                                   ((wave_buffer_internal.start_offset % 14) % 2)};
+        const auto start{start_frame * 8 + start_extra};
+
+        const auto end_frame{wave_buffer_internal.end_offset / 14};
+        const auto end_extra{wave_buffer_internal.end_offset % 14 == 0
+                                 ? 0
+                                 : (wave_buffer_internal.end_offset % 14) / 2 + 1 +
+                                       ((wave_buffer_internal.end_offset % 14) % 2)};
+        const auto end{end_frame * 8 + end_extra};
+
+        if (start > static_cast<s64>(wave_buffer_internal.size) ||
+            end > static_cast<s64>(wave_buffer_internal.size)) {
+            LOG_ERROR(Service_Audio, "Invalid ADPCM start/end wavebuffer sizes!");
+            error_info[0].error_code = Service::Audio::ERR_INVALID_UPDATE_DATA;
+            error_info[0].address = wave_buffer_internal.address;
+            return;
+        }
+    } break;
+
+    default:
+        break;
+    }
+
+    if (wave_buffer_internal.start_offset < 0 || wave_buffer_internal.end_offset < 0) {
+        LOG_ERROR(Service_Audio, "Invalid input start/end wavebuffer sizes!");
+        error_info[0].error_code = Service::Audio::ERR_INVALID_UPDATE_DATA;
+        error_info[0].address = wave_buffer_internal.address;
+        return;
+    }
+
+    wave_buffer.start_offset = wave_buffer_internal.start_offset;
+    wave_buffer.end_offset = wave_buffer_internal.end_offset;
+    wave_buffer.loop = wave_buffer_internal.loop;
+    wave_buffer.stream_ended = wave_buffer_internal.stream_ended;
+    wave_buffer.sent_to_DSP = false;
+    wave_buffer.loop_start_offset = wave_buffer_internal.loop_start;
+    wave_buffer.loop_end_offset = wave_buffer_internal.loop_end;
+    wave_buffer.loop_count = wave_buffer_internal.loop_count;
+
+    buffer_unmapped =
+        !pool_mapper.TryAttachBuffer(error_info[0], wave_buffer.buffer_address,
+                                     wave_buffer_internal.address, wave_buffer_internal.size);
+
+    if (sample_format_ == SampleFormat::Adpcm && behavior.IsAdpcmLoopContextBugFixed() &&
+        wave_buffer_internal.context_address != 0) {
+        buffer_unmapped = !pool_mapper.TryAttachBuffer(error_info[1], wave_buffer.context_address,
+                                                       wave_buffer_internal.context_address,
+                                                       wave_buffer_internal.context_size) ||
+                          data_unmapped;
+    } else {
+        wave_buffer.context_address.Setup(0, 0);
+    }
+}
+
+bool VoiceInfo::ShouldUpdateWaveBuffer(const WaveBufferInternal& wave_buffer_internal) const {
+    return !wave_buffer_internal.sent_to_DSP || buffer_unmapped;
+}
+
+void VoiceInfo::WriteOutStatus(OutStatus& out_status, const InParameter& params,
+                               std::span<VoiceState*> voice_states) {
+    if (params.is_new) {
+        is_new = true;
+    }
+
+    if (params.is_new || is_new) {
+        out_status.played_sample_count = 0;
+        out_status.wave_buffers_consumed = 0;
+        out_status.voice_dropped = false;
+    } else {
+        out_status.played_sample_count = voice_states[0]->played_sample_count;
+        out_status.wave_buffers_consumed = voice_states[0]->wave_buffers_consumed;
+        out_status.voice_dropped = voice_dropped;
+    }
+}
+
+bool VoiceInfo::ShouldSkip() const {
+    return !in_use || wave_buffer_count == 0 || data_unmapped || buffer_unmapped || voice_dropped;
+}
+
+bool VoiceInfo::HasAnyConnection() const {
+    return mix_id != UnusedMixId || splitter_id != UnusedSplitterId;
+}
+
+void VoiceInfo::FlushWaveBuffers(const u32 flush_count, std::span<VoiceState*> voice_states,
+                                 const s8 channel_count_) {
+    auto wave_index{wave_buffer_index};
+
+    for (size_t i = 0; i < flush_count; i++) {
+        wavebuffers[wave_index].sent_to_DSP = true;
+
+        for (s8 j = 0; j < channel_count_; j++) {
+            auto voice_state{voice_states[j]};
+            if (voice_state->wave_buffer_index == wave_index) {
+                voice_state->wave_buffer_index =
+                    (voice_state->wave_buffer_index + 1) % MaxWaveBuffers;
+                voice_state->wave_buffers_consumed++;
+            }
+            voice_state->wave_buffer_valid[wave_index] = false;
+        }
+
+        wave_index = (wave_index + 1) % MaxWaveBuffers;
+    }
+}
+
+bool VoiceInfo::UpdateParametersForCommandGeneration(std::span<VoiceState*> voice_states) {
+    if (flush_buffer_count > 0) {
+        FlushWaveBuffers(flush_buffer_count, voice_states, channel_count);
+        flush_buffer_count = 0;
+    }
+
+    switch (current_play_state) {
+    case ServerPlayState::Started:
+        for (u32 i = 0; i < MaxWaveBuffers; i++) {
+            if (!wavebuffers[i].sent_to_DSP) {
+                for (s8 channel = 0; channel < channel_count; channel++) {
+                    voice_states[channel]->wave_buffer_valid[i] = true;
+                }
+                wavebuffers[i].sent_to_DSP = true;
+            }
+        }
+
+        was_playing = false;
+
+        for (u32 i = 0; i < MaxWaveBuffers; i++) {
+            if (voice_states[0]->wave_buffer_valid[i]) {
+                return true;
+            }
+        }
+        break;
+
+    case ServerPlayState::Stopped:
+    case ServerPlayState::Paused:
+        for (auto& wavebuffer : wavebuffers) {
+            if (!wavebuffer.sent_to_DSP) {
+                wavebuffer.buffer_address.GetReference(true);
+                wavebuffer.context_address.GetReference(true);
+            }
+        }
+
+        if (sample_format == SampleFormat::Adpcm && data_address.GetCpuAddr() != 0) {
+            data_address.GetReference(true);
+        }
+
+        was_playing = last_play_state == ServerPlayState::Started;
+        break;
+
+    case ServerPlayState::RequestStop:
+        for (u32 i = 0; i < MaxWaveBuffers; i++) {
+            wavebuffers[i].sent_to_DSP = true;
+
+            for (s8 channel = 0; channel < channel_count; channel++) {
+                if (voice_states[channel]->wave_buffer_valid[i]) {
+                    voice_states[channel]->wave_buffer_index =
+                        (voice_states[channel]->wave_buffer_index + 1) % MaxWaveBuffers;
+                    voice_states[channel]->wave_buffers_consumed++;
+                }
+                voice_states[channel]->wave_buffer_valid[i] = false;
+            }
+        }
+
+        for (s8 channel = 0; channel < channel_count; channel++) {
+            voice_states[channel]->offset = 0;
+            voice_states[channel]->played_sample_count = 0;
+            voice_states[channel]->adpcm_context = {};
+            voice_states[channel]->sample_history.fill(0);
+            voice_states[channel]->fraction = 0;
+        }
+
+        current_play_state = ServerPlayState::Stopped;
+        was_playing = last_play_state == ServerPlayState::Started;
+        break;
+    }
+
+    return was_playing;
+}
+
+bool VoiceInfo::UpdateForCommandGeneration(VoiceContext& voice_context) {
+    std::array<VoiceState*, MaxChannels> voice_states{};
+
+    if (is_new) {
+        ResetResources(voice_context);
+        prev_volume = volume;
+        is_new = false;
+    }
+
+    for (s8 channel = 0; channel < channel_count; channel++) {
+        voice_states[channel] = &voice_context.GetDspSharedState(channel_resource_ids[channel]);
+    }
+
+    return UpdateParametersForCommandGeneration(voice_states);
+}
+
+void VoiceInfo::ResetResources(VoiceContext& voice_context) const {
+    for (s8 channel = 0; channel < channel_count; channel++) {
+        auto& state{voice_context.GetDspSharedState(channel_resource_ids[channel])};
+        state = {};
+
+        auto& channel_resource{voice_context.GetChannelResource(channel_resource_ids[channel])};
+        channel_resource.prev_mix_volumes = channel_resource.mix_volumes;
+    }
+}
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/voice/voice_info.h b/src/audio_core/renderer/voice/voice_info.h
new file mode 100644
index 0000000000..896723e0c7
--- /dev/null
+++ b/src/audio_core/renderer/voice/voice_info.h
@@ -0,0 +1,378 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+#include <bitset>
+
+#include "audio_core/common/common.h"
+#include "audio_core/common/wave_buffer.h"
+#include "audio_core/renderer/behavior/behavior_info.h"
+#include "audio_core/renderer/memory/address_info.h"
+#include "common/common_types.h"
+
+namespace AudioCore::AudioRenderer {
+class PoolMapper;
+class VoiceContext;
+struct VoiceState;
+
+/**
+ * Represents one voice. Voices are essentially noises, and they can be further mixed and have
+ * effects applied to them, but voices are the basis of all sounds.
+ */
+class VoiceInfo {
+public:
+    enum class ServerPlayState {
+        Started,
+        Stopped,
+        RequestStop,
+        Paused,
+    };
+
+    struct Flags {
+        u8 IsVoicePlayedSampleCountResetAtLoopPointSupported : 1;
+        u8 IsVoicePitchAndSrcSkippedSupported : 1;
+    };
+
+    /**
+     * A wavebuffer contains information on the data source buffers.
+     */
+    struct WaveBuffer {
+        void Copy(WaveBufferVersion1& other) {
+            other.buffer = buffer_address.GetReference(true);
+            other.buffer_size = buffer_address.GetSize();
+            other.start_offset = start_offset;
+            other.end_offset = end_offset;
+            other.loop = loop;
+            other.stream_ended = stream_ended;
+
+            if (context_address.GetCpuAddr()) {
+                other.context = context_address.GetReference(true);
+                other.context_size = context_address.GetSize();
+            } else {
+                other.context = CpuAddr(0);
+                other.context_size = 0;
+            }
+        }
+
+        void Copy(WaveBufferVersion2& other) {
+            other.buffer = buffer_address.GetReference(true);
+            other.buffer_size = buffer_address.GetSize();
+            other.start_offset = start_offset;
+            other.end_offset = end_offset;
+            other.loop_start_offset = loop_start_offset;
+            other.loop_end_offset = loop_end_offset;
+            other.loop = loop;
+            other.loop_count = loop_count;
+            other.stream_ended = stream_ended;
+
+            if (context_address.GetCpuAddr()) {
+                other.context = context_address.GetReference(true);
+                other.context_size = context_address.GetSize();
+            } else {
+                other.context = CpuAddr(0);
+                other.context_size = 0;
+            }
+        }
+
+        void Initialize() {
+            buffer_address.Setup(0, 0);
+            context_address.Setup(0, 0);
+            start_offset = 0;
+            end_offset = 0;
+            loop = false;
+            stream_ended = false;
+            sent_to_DSP = true;
+            loop_start_offset = 0;
+            loop_end_offset = 0;
+            loop_count = 0;
+        }
+        /// Game memory address of the wavebuffer data
+        AddressInfo buffer_address{0, 0};
+        /// Context for decoding, used for ADPCM
+        AddressInfo context_address{0, 0};
+        /// Starting offset for the wavebuffer
+        u32 start_offset{};
+        /// Ending offset the wavebuffer
+        u32 end_offset{};
+        /// Should this wavebuffer loop?
+        bool loop{};
+        /// Has this wavebuffer ended?
+        bool stream_ended{};
+        /// Has this wavebuffer been sent to the AudioRenderer?
+        bool sent_to_DSP{true};
+        /// Starting offset when looping, can differ from start_offset
+        u32 loop_start_offset{};
+        /// Ending offset when looping, can differ from end_offset
+        u32 loop_end_offset{};
+        /// Number of times to loop this wavebuffer
+        s32 loop_count{};
+    };
+
+    struct WaveBufferInternal {
+        /* 0x00 */ CpuAddr address;
+        /* 0x08 */ u64 size;
+        /* 0x10 */ s32 start_offset;
+        /* 0x14 */ s32 end_offset;
+        /* 0x18 */ bool loop;
+        /* 0x19 */ bool stream_ended;
+        /* 0x1A */ bool sent_to_DSP;
+        /* 0x1C */ s32 loop_count;
+        /* 0x20 */ CpuAddr context_address;
+        /* 0x28 */ u64 context_size;
+        /* 0x30 */ u32 loop_start;
+        /* 0x34 */ u32 loop_end;
+    };
+    static_assert(sizeof(WaveBufferInternal) == 0x38,
+                  "VoiceInfo::WaveBufferInternal has the wrong size!");
+
+    struct BiquadFilterParameter {
+        /* 0x00 */ bool enabled;
+        /* 0x02 */ std::array<s16, 3> b;
+        /* 0x08 */ std::array<s16, 2> a;
+    };
+    static_assert(sizeof(BiquadFilterParameter) == 0xC,
+                  "VoiceInfo::BiquadFilterParameter has the wrong size!");
+
+    struct InParameter {
+        /* 0x000 */ u32 id;
+        /* 0x004 */ u32 node_id;
+        /* 0x008 */ bool is_new;
+        /* 0x009 */ bool in_use;
+        /* 0x00A */ PlayState play_state;
+        /* 0x00B */ SampleFormat sample_format;
+        /* 0x00C */ u32 sample_rate;
+        /* 0x010 */ s32 priority;
+        /* 0x014 */ s32 sort_order;
+        /* 0x018 */ u32 channel_count;
+        /* 0x01C */ f32 pitch;
+        /* 0x020 */ f32 volume;
+        /* 0x024 */ std::array<BiquadFilterParameter, MaxBiquadFilters> biquads;
+        /* 0x03C */ u32 wave_buffer_count;
+        /* 0x040 */ u16 wave_buffer_index;
+        /* 0x042 */ char unk042[0x6];
+        /* 0x048 */ CpuAddr src_data_address;
+        /* 0x050 */ u64 src_data_size;
+        /* 0x058 */ u32 mix_id;
+        /* 0x05C */ u32 splitter_id;
+        /* 0x060 */ std::array<WaveBufferInternal, MaxWaveBuffers> wave_buffer_internal;
+        /* 0x140 */ std::array<u32, MaxChannels> channel_resource_ids;
+        /* 0x158 */ bool clear_voice_drop;
+        /* 0x159 */ u8 flush_buffer_count;
+        /* 0x15A */ char unk15A[0x2];
+        /* 0x15C */ Flags flags;
+        /* 0x15D */ char unk15D[0x1];
+        /* 0x15E */ SrcQuality src_quality;
+        /* 0x15F */ char unk15F[0x11];
+    };
+    static_assert(sizeof(InParameter) == 0x170, "VoiceInfo::InParameter has the wrong size!");
+
+    struct OutStatus {
+        /* 0x00 */ u64 played_sample_count;
+        /* 0x08 */ u32 wave_buffers_consumed;
+        /* 0x0C */ bool voice_dropped;
+    };
+    static_assert(sizeof(OutStatus) == 0x10, "OutStatus::InParameter has the wrong size!");
+
+    VoiceInfo();
+
+    /**
+     * Initialize this voice.
+     */
+    void Initialize();
+
+    /**
+     * Does this voice ned an update?
+     *
+     * @param params - Input parametetrs to check matching.
+     * @return True if this voice needs an update, otherwise false.
+     */
+    bool ShouldUpdateParameters(const InParameter& params) const;
+
+    /**
+     * Update the parameters of this voice.
+     *
+     * @param error_info  - Output error code.
+     * @param params      - Input parametters to udpate from.
+     * @param pool_mapper - Used to map buffers.
+     * @param behavior   - behavior to check supported features.
+     */
+    void UpdateParameters(BehaviorInfo::ErrorInfo& error_info, const InParameter& params,
+                          const PoolMapper& pool_mapper, const BehaviorInfo& behavior);
+
+    /**
+     * Update the current play state.
+     *
+     * @param state - New play state for this voice.
+     */
+    void UpdatePlayState(PlayState state);
+
+    /**
+     * Update the current sample rate conversion quality.
+     *
+     * @param quality - New quality.
+     */
+    void UpdateSrcQuality(SrcQuality quality);
+
+    /**
+     * Update all wavebuffers.
+     *
+     * @param error_infos - Output 2D array of errors, 2 per wavebuffer.
+     * @param error_count - Number of errors provided. Unused.
+     * @param params - Input parametters to be used for the update.
+     * @param voice_states - The voice states for each channel in this voice to be updated.
+     * @param pool_mapper - Used to map the wavebuffers.
+     * @param behavior - Used to check for supported features.
+     */
+    void UpdateWaveBuffers(std::span<std::array<BehaviorInfo::ErrorInfo, 2>> error_infos,
+                           u32 error_count, const InParameter& params,
+                           std::span<VoiceState*> voice_states, const PoolMapper& pool_mapper,
+                           const BehaviorInfo& behavior);
+
+    /**
+     * Update a wavebuffer.
+     *
+     * @param error_infos          - Output array of errors.
+     * @param wave_buffer          - The wavebuffer to be updated.
+     * @param wave_buffer_internal - Input parametters to be used for the update.
+     * @param sample_format        - Sample format of the wavebuffer.
+     * @param valid                - Is this wavebuffer valid?
+     * @param pool_mapper          - Used to map the wavebuffers.
+     * @param behavior            - Used to check for supported features.
+     */
+    void UpdateWaveBuffer(std::span<BehaviorInfo::ErrorInfo> error_info, WaveBuffer& wave_buffer,
+                          const WaveBufferInternal& wave_buffer_internal,
+                          SampleFormat sample_format, bool valid, const PoolMapper& pool_mapper,
+                          const BehaviorInfo& behavior);
+
+    /**
+     * Check if the input wavebuffer needs an update.
+     *
+     * @param wave_buffer_internal - Input wavebuffer parameters to check.
+     * @return True if the given wavebuffer needs an update, otherwise false.
+     */
+    bool ShouldUpdateWaveBuffer(const WaveBufferInternal& wave_buffer_internal) const;
+
+    /**
+     * Write the number of played samples, number of consumed wavebuffers and if this voice was
+     * dropped, to the given out_status.
+     *
+     * @param out_status   - Output status to be written to.
+     * @param in_params    - Input parameters to check if the wavebuffer is new.
+     * @param voice_states - Current host voice states for this voice, source of the output.
+     */
+    void WriteOutStatus(OutStatus& out_status, const InParameter& in_params,
+                        std::span<VoiceState*> voice_states);
+
+    /**
+     * Check if this voice should be skipped for command generation.
+     * Checks various things such as usage state, whether data is mapped etc.
+     *
+     * @return True if this voice should not be generated, otherwise false.
+     */
+    bool ShouldSkip() const;
+
+    /**
+     * Check if this voice has any mixing connections.
+     *
+     * @return True if this voice participes in mixing, otherwise false.
+     */
+    bool HasAnyConnection() const;
+
+    /**
+     * Flush flush_count wavebuffers, marking them as consumed.
+     *
+     * @param flush_count   - Number of wavebuffers to flush.
+     * @param voice_states  - Voice states for these wavebuffers.
+     * @param channel_count - Number of active channels.
+     */
+    void FlushWaveBuffers(u32 flush_count, std::span<VoiceState*> voice_states, s8 channel_count);
+
+    /**
+     * Update this voice's parameters on command generation,
+     * updating voice states and flushing if needed.
+     *
+     * @param voice_states  - Voice states for these wavebuffers.
+     * @return True if this voice should be generated, otherwise false.
+     */
+    bool UpdateParametersForCommandGeneration(std::span<VoiceState*> voice_states);
+
+    /**
+     * Update this voice on command generation.
+     *
+     * @param voice_states  - Voice states for these wavebuffers.
+     * @return True if this voice should be generated, otherwise false.
+     */
+    bool UpdateForCommandGeneration(VoiceContext& voice_context);
+
+    /**
+     * Reset the AudioRenderer-side voice states, and the channel resources for this voice.
+     *
+     * @param voice_context - Context from which to get the resources.
+     */
+    void ResetResources(VoiceContext& voice_context) const;
+
+    /// Is this voice in use?
+    bool in_use{};
+    /// Is this voice new?
+    bool is_new{};
+    /// Was this voice last playing? Used for depopping
+    bool was_playing{};
+    /// Sample format of the wavebuffers in this voice
+    SampleFormat sample_format{};
+    /// Sample rate of the wavebuffers in this voice
+    u32 sample_rate{};
+    /// Number of channels in this voice
+    s8 channel_count{};
+    /// Id of this voice
+    u32 id{};
+    /// Node id of this voice
+    u32 node_id{};
+    /// Mix id this voice is mixed to
+    u32 mix_id{};
+    /// Play state of this voice
+    ServerPlayState current_play_state{ServerPlayState::Stopped};
+    /// Last play state of this voice
+    ServerPlayState last_play_state{ServerPlayState::Started};
+    /// Priority of this voice, lower is higher
+    s32 priority{};
+    /// Sort order of this voice, used when same priority
+    s32 sort_order{};
+    /// Pitch of this voice (for sample rate conversion)
+    f32 pitch{};
+    /// Current volume of this voice
+    f32 volume{};
+    /// Previous volume of this voice
+    f32 prev_volume{};
+    /// Biquad filters for generating filter commands on this voice
+    std::array<BiquadFilterParameter, MaxBiquadFilters> biquads{};
+    /// Number of active wavebuffers
+    u32 wave_buffer_count{};
+    /// Current playing wavebuffer index
+    u16 wave_buffer_index{};
+    /// Flags controlling decode behavior
+    u16 flags{};
+    /// Game memory for ADPCM coefficients
+    AddressInfo data_address{0, 0};
+    /// Wavebuffers
+    std::array<WaveBuffer, MaxWaveBuffers> wavebuffers{};
+    /// Channel resources for this voice
+    std::array<u32, MaxChannels> channel_resource_ids{};
+    /// Splitter id this voice is connected with
+    s32 splitter_id{UnusedSplitterId};
+    /// Sample rate conversion quality
+    SrcQuality src_quality{SrcQuality::Medium};
+    /// Was this voice dropped due to limited time?
+    bool voice_dropped{};
+    /// Is this voice's coefficient (data_address) unmapped?
+    bool data_unmapped{};
+    /// Is this voice's buffers (wavebuffer data and ADPCM context) unmapped?
+    bool buffer_unmapped{};
+    /// Initialisation state of the biquads
+    std::array<bool, MaxBiquadFilters> biquad_initialized{};
+    /// Number of wavebuffers to flush
+    u8 flush_buffer_count{};
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/renderer/voice/voice_state.h b/src/audio_core/renderer/voice/voice_state.h
new file mode 100644
index 0000000000..d5497e2fb6
--- /dev/null
+++ b/src/audio_core/renderer/voice/voice_state.h
@@ -0,0 +1,70 @@
+// SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <array>
+
+#include "audio_core/common/common.h"
+#include "common/common_types.h"
+#include "common/fixed_point.h"
+
+namespace AudioCore::AudioRenderer {
+/**
+ * Holds a state for a voice. One is kept host-side, and one is used by the AudioRenderer,
+ * host-side is updated on the next iteration.
+ */
+struct VoiceState {
+    /**
+     * State of the voice's biquad filter.
+     */
+    struct BiquadFilterState {
+        Common::FixedPoint<50, 14> s0;
+        Common::FixedPoint<50, 14> s1;
+        Common::FixedPoint<50, 14> s2;
+        Common::FixedPoint<50, 14> s3;
+    };
+
+    /**
+     * Context for ADPCM decoding.
+     */
+    struct AdpcmContext {
+        u16 header;
+        s16 yn0;
+        s16 yn1;
+    };
+
+    /// Number of samples played
+    u64 played_sample_count;
+    /// Current offset from the starting offset
+    u32 offset;
+    /// Currently active wavebuffer index
+    u32 wave_buffer_index;
+    /// Array of which wavebuffers are currently valid
+
+    std::array<bool, MaxWaveBuffers> wave_buffer_valid;
+    /// Number of wavebuffers consumed, given back to the game
+    u32 wave_buffers_consumed;
+    /// History of samples, used for rate conversion
+
+    std::array<s16, MaxWaveBuffers * 2> sample_history;
+    /// Current read fraction, used for resampling
+    Common::FixedPoint<49, 15> fraction;
+    /// Current adpcm context
+    AdpcmContext adpcm_context;
+    /// Current biquad states, used when filtering
+
+    std::array<std::array<BiquadFilterState, MaxBiquadFilters>, MaxBiquadFilters> biquad_states;
+    /// Previous samples
+    std::array<s32, MaxMixBuffers> previous_samples;
+    /// Unused
+    u32 external_context_size;
+    /// Unused
+    bool external_context_enabled;
+    /// Was this voice dropped?
+    bool voice_dropped;
+    /// Number of times the wavebuffer has looped
+    s32 loop_count;
+};
+
+} // namespace AudioCore::AudioRenderer
diff --git a/src/audio_core/sdl2_sink.cpp b/src/audio_core/sdl2_sink.cpp
deleted file mode 100644
index a10ba40441..0000000000
--- a/src/audio_core/sdl2_sink.cpp
+++ /dev/null
@@ -1,160 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include <algorithm>
-#include <atomic>
-#include <cstring>
-#include "audio_core/sdl2_sink.h"
-#include "audio_core/stream.h"
-#include "common/assert.h"
-#include "common/logging/log.h"
-//#include "common/settings.h"
-
-// Ignore -Wimplicit-fallthrough due to https://github.com/libsdl-org/SDL/issues/4307
-#ifdef __clang__
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wimplicit-fallthrough"
-#endif
-#include <SDL.h>
-#ifdef __clang__
-#pragma clang diagnostic pop
-#endif
-
-namespace AudioCore {
-
-class SDLSinkStream final : public SinkStream {
-public:
-    SDLSinkStream(u32 sample_rate, u32 num_channels_, const std::string& output_device)
-        : num_channels{std::min(num_channels_, 6u)} {
-
-        SDL_AudioSpec spec;
-        spec.freq = sample_rate;
-        spec.channels = static_cast<u8>(num_channels);
-        spec.format = AUDIO_S16SYS;
-        spec.samples = 4096;
-        spec.callback = nullptr;
-
-        SDL_AudioSpec obtained;
-        if (output_device.empty()) {
-            dev = SDL_OpenAudioDevice(nullptr, 0, &spec, &obtained, 0);
-        } else {
-            dev = SDL_OpenAudioDevice(output_device.c_str(), 0, &spec, &obtained, 0);
-        }
-
-        if (dev == 0) {
-            LOG_CRITICAL(Audio_Sink, "Error opening sdl audio device: {}", SDL_GetError());
-            return;
-        }
-
-        SDL_PauseAudioDevice(dev, 0);
-    }
-
-    ~SDLSinkStream() override {
-        if (dev == 0) {
-            return;
-        }
-
-        SDL_CloseAudioDevice(dev);
-    }
-
-    void EnqueueSamples(u32 source_num_channels, const std::vector<s16>& samples) override {
-        if (source_num_channels > num_channels) {
-            // Downsample 6 channels to 2
-            ASSERT_MSG(source_num_channels == 6, "Channel count must be 6");
-
-            std::vector<s16> buf;
-            buf.reserve(samples.size() * num_channels / source_num_channels);
-            for (std::size_t i = 0; i < samples.size(); i += source_num_channels) {
-                // Downmixing implementation taken from the ATSC standard
-                const s16 left{samples[i + 0]};
-                const s16 right{samples[i + 1]};
-                const s16 center{samples[i + 2]};
-                const s16 surround_left{samples[i + 4]};
-                const s16 surround_right{samples[i + 5]};
-                // Not used in the ATSC reference implementation
-                [[maybe_unused]] const s16 low_frequency_effects{samples[i + 3]};
-
-                constexpr s32 clev{707}; // center mixing level coefficient
-                constexpr s32 slev{707}; // surround mixing level coefficient
-
-                buf.push_back(static_cast<s16>(left + (clev * center / 1000) +
-                                               (slev * surround_left / 1000)));
-                buf.push_back(static_cast<s16>(right + (clev * center / 1000) +
-                                               (slev * surround_right / 1000)));
-            }
-            int ret = SDL_QueueAudio(dev, static_cast<const void*>(buf.data()),
-                                     static_cast<u32>(buf.size() * sizeof(s16)));
-            if (ret < 0)
-                LOG_WARNING(Audio_Sink, "Could not queue audio buffer: {}", SDL_GetError());
-            return;
-        }
-
-        int ret = SDL_QueueAudio(dev, static_cast<const void*>(samples.data()),
-                                 static_cast<u32>(samples.size() * sizeof(s16)));
-        if (ret < 0)
-            LOG_WARNING(Audio_Sink, "Could not queue audio buffer: {}", SDL_GetError());
-    }
-
-    std::size_t SamplesInQueue(u32 channel_count) const override {
-        if (dev == 0)
-            return 0;
-
-        return SDL_GetQueuedAudioSize(dev) / (channel_count * sizeof(s16));
-    }
-
-    void Flush() override {
-        should_flush = true;
-    }
-
-    u32 GetNumChannels() const {
-        return num_channels;
-    }
-
-private:
-    SDL_AudioDeviceID dev = 0;
-    u32 num_channels{};
-    std::atomic<bool> should_flush{};
-};
-
-SDLSink::SDLSink(std::string_view target_device_name) {
-    if (!SDL_WasInit(SDL_INIT_AUDIO)) {
-        if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
-            LOG_CRITICAL(Audio_Sink, "SDL_InitSubSystem audio failed: {}", SDL_GetError());
-            return;
-        }
-    }
-
-    if (target_device_name != auto_device_name && !target_device_name.empty()) {
-        output_device = target_device_name;
-    } else {
-        output_device.clear();
-    }
-}
-
-SDLSink::~SDLSink() = default;
-
-SinkStream& SDLSink::AcquireSinkStream(u32 sample_rate, u32 num_channels, const std::string&) {
-    sink_streams.push_back(
-        std::make_unique<SDLSinkStream>(sample_rate, num_channels, output_device));
-    return *sink_streams.back();
-}
-
-std::vector<std::string> ListSDLSinkDevices() {
-    std::vector<std::string> device_list;
-
-    if (!SDL_WasInit(SDL_INIT_AUDIO)) {
-        if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
-            LOG_CRITICAL(Audio_Sink, "SDL_InitSubSystem audio failed: {}", SDL_GetError());
-            return {};
-        }
-    }
-
-    const int device_count = SDL_GetNumAudioDevices(0);
-    for (int i = 0; i < device_count; ++i) {
-        device_list.emplace_back(SDL_GetAudioDeviceName(i, 0));
-    }
-
-    return device_list;
-}
-
-} // namespace AudioCore
diff --git a/src/audio_core/sdl2_sink.h b/src/audio_core/sdl2_sink.h
deleted file mode 100644
index f1dd1d677c..0000000000
--- a/src/audio_core/sdl2_sink.h
+++ /dev/null
@@ -1,28 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include <string>
-#include <vector>
-
-#include "audio_core/sink.h"
-
-namespace AudioCore {
-
-class SDLSink final : public Sink {
-public:
-    explicit SDLSink(std::string_view device_id);
-    ~SDLSink() override;
-
-    SinkStream& AcquireSinkStream(u32 sample_rate, u32 num_channels,
-                                  const std::string& name) override;
-
-private:
-    std::string output_device;
-    std::vector<SinkStreamPtr> sink_streams;
-};
-
-std::vector<std::string> ListSDLSinkDevices();
-
-} // namespace AudioCore
diff --git a/src/audio_core/sink.h b/src/audio_core/sink.h
deleted file mode 100644
index 3c03554fa3..0000000000
--- a/src/audio_core/sink.h
+++ /dev/null
@@ -1,30 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include <memory>
-#include <string>
-
-#include "audio_core/sink_stream.h"
-#include "common/common_types.h"
-
-namespace AudioCore {
-
-constexpr char auto_device_name[] = "auto";
-
-/**
- * This class is an interface for an audio sink. An audio sink accepts samples in stereo signed
- * PCM16 format to be output. Sinks *do not* handle resampling and expect the correct sample rate.
- * They are dumb outputs.
- */
-class Sink {
-public:
-    virtual ~Sink() = default;
-    virtual SinkStream& AcquireSinkStream(u32 sample_rate, u32 num_channels,
-                                          const std::string& name) = 0;
-};
-
-using SinkPtr = std::unique_ptr<Sink>;
-
-} // namespace AudioCore
diff --git a/src/audio_core/sink/cubeb_sink.cpp b/src/audio_core/sink/cubeb_sink.cpp
new file mode 100644
index 0000000000..a4e28de6de
--- /dev/null
+++ b/src/audio_core/sink/cubeb_sink.cpp
@@ -0,0 +1,651 @@
+// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <algorithm>
+#include <atomic>
+#include <span>
+
+#include "audio_core/audio_core.h"
+#include "audio_core/audio_event.h"
+#include "audio_core/audio_manager.h"
+#include "audio_core/sink/cubeb_sink.h"
+#include "audio_core/sink/sink_stream.h"
+#include "common/assert.h"
+#include "common/fixed_point.h"
+#include "common/logging/log.h"
+#include "common/reader_writer_queue.h"
+#include "common/ring_buffer.h"
+#include "common/settings.h"
+#include "core/core.h"
+
+#ifdef _WIN32
+#include <objbase.h>
+#undef CreateEvent
+#endif
+
+namespace AudioCore::Sink {
+/**
+ * Cubeb sink stream, responsible for sinking samples to hardware.
+ */
+class CubebSinkStream final : public SinkStream {
+public:
+    /**
+     * Create a new sink stream.
+     *
+     * @param ctx_             - Cubeb context to create this stream with.
+     * @param device_channels_ - Number of channels supported by the hardware.
+     * @param system_channels_ - Number of channels the audio systems expect.
+     * @param output_device    - Cubeb output device id.
+     * @param input_device     - Cubeb input device id.
+     * @param name_            - Name of this stream.
+     * @param type_            - Type of this stream.
+     * @param system_          - Core system.
+     * @param event            - Event used only for audio renderer, signalled on buffer consume.
+     */
+    CubebSinkStream(cubeb* ctx_, const u32 device_channels_, const u32 system_channels_,
+                    cubeb_devid output_device, cubeb_devid input_device, const std::string& name_,
+                    const StreamType type_, Core::System& system_)
+        : ctx{ctx_}, type{type_}, system{system_} {
+#ifdef _WIN32
+        CoInitializeEx(nullptr, COINIT_MULTITHREADED);
+#endif
+        name = name_;
+        device_channels = device_channels_;
+        system_channels = system_channels_;
+
+        cubeb_stream_params params{};
+        params.rate = TargetSampleRate;
+        params.channels = device_channels;
+        params.format = CUBEB_SAMPLE_S16LE;
+        params.prefs = CUBEB_STREAM_PREF_NONE;
+        switch (params.channels) {
+        case 1:
+            params.layout = CUBEB_LAYOUT_MONO;
+            break;
+        case 2:
+            params.layout = CUBEB_LAYOUT_STEREO;
+            break;
+        case 6:
+            params.layout = CUBEB_LAYOUT_3F2_LFE;
+            break;
+        }
+
+        u32 minimum_latency{0};
+        const auto latency_error = cubeb_get_min_latency(ctx, &params, &minimum_latency);
+        if (latency_error != CUBEB_OK) {
+            LOG_CRITICAL(Audio_Sink, "Error getting minimum latency, error: {}", latency_error);
+            minimum_latency = 256U;
+        }
+
+        minimum_latency = std::max(minimum_latency, 256u);
+
+        playing_buffer.consumed = true;
+
+        LOG_DEBUG(Service_Audio,
+                  "Opening cubeb stream {} type {} with: rate {} channels {} (system channels {}) "
+                  "latency {}",
+                  name, type, params.rate, params.channels, system_channels, minimum_latency);
+
+        auto init_error{0};
+        if (type == StreamType::In) {
+            init_error = cubeb_stream_init(ctx, &stream_backend, name.c_str(), input_device,
+                                           &params, output_device, nullptr, minimum_latency,
+                                           &CubebSinkStream::DataCallback,
+                                           &CubebSinkStream::StateCallback, this);
+        } else {
+            init_error = cubeb_stream_init(ctx, &stream_backend, name.c_str(), input_device,
+                                           nullptr, output_device, &params, minimum_latency,
+                                           &CubebSinkStream::DataCallback,
+                                           &CubebSinkStream::StateCallback, this);
+        }
+
+        if (init_error != CUBEB_OK) {
+            LOG_CRITICAL(Audio_Sink, "Error initializing cubeb stream, error: {}", init_error);
+            return;
+        }
+    }
+
+    /**
+     * Destroy the sink stream.
+     */
+    ~CubebSinkStream() override {
+        LOG_DEBUG(Service_Audio, "Destructing cubeb stream {}", name);
+
+        if (!ctx) {
+            return;
+        }
+
+        Finalize();
+
+#ifdef _WIN32
+        CoUninitialize();
+#endif
+    }
+
+    /**
+     * Finalize the sink stream.
+     */
+    void Finalize() override {
+        Stop();
+        cubeb_stream_destroy(stream_backend);
+    }
+
+    /**
+     * Start the sink stream.
+     *
+     * @param resume - Set to true if this is resuming the stream a previously-active stream.
+     *                 Default false.
+     */
+    void Start(const bool resume = false) override {
+        if (!ctx) {
+            return;
+        }
+
+        if (resume && was_playing) {
+            if (cubeb_stream_start(stream_backend) != CUBEB_OK) {
+                LOG_CRITICAL(Audio_Sink, "Error starting cubeb stream");
+            }
+            paused = false;
+        } else if (!resume) {
+            if (cubeb_stream_start(stream_backend) != CUBEB_OK) {
+                LOG_CRITICAL(Audio_Sink, "Error starting cubeb stream");
+            }
+            paused = false;
+        }
+    }
+
+    /**
+     * Stop the sink stream.
+     */
+    void Stop() override {
+        if (!ctx) {
+            return;
+        }
+
+        if (cubeb_stream_stop(stream_backend) != CUBEB_OK) {
+            LOG_CRITICAL(Audio_Sink, "Error stopping cubeb stream");
+        }
+
+        was_playing.store(!paused);
+        paused = true;
+    }
+
+    /**
+     * Append a new buffer and its samples to a waiting queue to play.
+     *
+     * @param buffer  - Audio buffer information to be queued.
+     * @param samples - The s16 samples to be queue for playback.
+     */
+    void AppendBuffer(::AudioCore::Sink::SinkBuffer& buffer, std::vector<s16>& samples) override {
+        if (type == StreamType::In) {
+            queue.enqueue(buffer);
+            queued_buffers++;
+        } else {
+            constexpr s32 min{std::numeric_limits<s16>::min()};
+            constexpr s32 max{std::numeric_limits<s16>::max()};
+
+            auto yuzu_volume{Settings::Volume()};
+            auto volume{system_volume * device_volume * yuzu_volume};
+
+            if (system_channels == 6 && device_channels == 2) {
+                // We're given 6 channels, but our device only outputs 2, so downmix.
+                constexpr std::array<f32, 4> down_mix_coeff{1.0f, 0.707f, 0.251f, 0.707f};
+
+                for (u32 read_index = 0, write_index = 0; read_index < samples.size();
+                     read_index += system_channels, write_index += device_channels) {
+                    const auto left_sample{
+                        ((Common::FixedPoint<49, 15>(
+                              samples[read_index + static_cast<u32>(Channels::FrontLeft)]) *
+                              down_mix_coeff[0] +
+                          samples[read_index + static_cast<u32>(Channels::Center)] *
+                              down_mix_coeff[1] +
+                          samples[read_index + static_cast<u32>(Channels::LFE)] *
+                              down_mix_coeff[2] +
+                          samples[read_index + static_cast<u32>(Channels::BackLeft)] *
+                              down_mix_coeff[3]) *
+                         volume)
+                            .to_int()};
+
+                    const auto right_sample{
+                        ((Common::FixedPoint<49, 15>(
+                              samples[read_index + static_cast<u32>(Channels::FrontRight)]) *
+                              down_mix_coeff[0] +
+                          samples[read_index + static_cast<u32>(Channels::Center)] *
+                              down_mix_coeff[1] +
+                          samples[read_index + static_cast<u32>(Channels::LFE)] *
+                              down_mix_coeff[2] +
+                          samples[read_index + static_cast<u32>(Channels::BackRight)] *
+                              down_mix_coeff[3]) *
+                         volume)
+                            .to_int()};
+
+                    samples[write_index + static_cast<u32>(Channels::FrontLeft)] =
+                        static_cast<s16>(std::clamp(left_sample, min, max));
+                    samples[write_index + static_cast<u32>(Channels::FrontRight)] =
+                        static_cast<s16>(std::clamp(right_sample, min, max));
+                }
+
+                samples.resize(samples.size() / system_channels * device_channels);
+
+            } else if (system_channels == 2 && device_channels == 6) {
+                // We need moar samples! Not all games will provide 6 channel audio.
+                // TODO: Implement some upmixing here. Currently just passthrough, with other
+                // channels left as silence.
+                std::vector<s16> new_samples(samples.size() / system_channels * device_channels, 0);
+
+                for (u32 read_index = 0, write_index = 0; read_index < samples.size();
+                     read_index += system_channels, write_index += device_channels) {
+                    const auto left_sample{static_cast<s16>(std::clamp(
+                        static_cast<s32>(
+                            static_cast<f32>(
+                                samples[read_index + static_cast<u32>(Channels::FrontLeft)]) *
+                            volume),
+                        min, max))};
+
+                    new_samples[write_index + static_cast<u32>(Channels::FrontLeft)] = left_sample;
+
+                    const auto right_sample{static_cast<s16>(std::clamp(
+                        static_cast<s32>(
+                            static_cast<f32>(
+                                samples[read_index + static_cast<u32>(Channels::FrontRight)]) *
+                            volume),
+                        min, max))};
+
+                    new_samples[write_index + static_cast<u32>(Channels::FrontRight)] =
+                        right_sample;
+                }
+                samples = std::move(new_samples);
+
+            } else if (volume != 1.0f) {
+                for (u32 i = 0; i < samples.size(); i++) {
+                    samples[i] = static_cast<s16>(std::clamp(
+                        static_cast<s32>(static_cast<f32>(samples[i]) * volume), min, max));
+                }
+            }
+
+            samples_buffer.Push(samples);
+            queue.enqueue(buffer);
+            queued_buffers++;
+        }
+    }
+
+    /**
+     * Release a buffer. Audio In only, will fill a buffer with recorded samples.
+     *
+     * @param num_samples - Maximum number of samples to receive.
+     * @return Vector of recorded samples. May have fewer than num_samples.
+     */
+    std::vector<s16> ReleaseBuffer(const u64 num_samples) override {
+        static constexpr s32 min = std::numeric_limits<s16>::min();
+        static constexpr s32 max = std::numeric_limits<s16>::max();
+
+        auto samples{samples_buffer.Pop(num_samples)};
+
+        // TODO: Up-mix to 6 channels if the game expects it.
+        // For audio input this is unlikely to ever be the case though.
+
+        // Incoming mic volume seems to always be very quiet, so multiply by an additional 8 here.
+        // TODO: Play with this and find something that works better.
+        auto volume{system_volume * device_volume * 8};
+        for (u32 i = 0; i < samples.size(); i++) {
+            samples[i] = static_cast<s16>(
+                std::clamp(static_cast<s32>(static_cast<f32>(samples[i]) * volume), min, max));
+        }
+
+        if (samples.size() < num_samples) {
+            samples.resize(num_samples, 0);
+        }
+        return samples;
+    }
+
+    /**
+     * Check if a certain buffer has been consumed (fully played).
+     *
+     * @param tag - Unique tag of a buffer to check for.
+     * @return True if the buffer has been played, otherwise false.
+     */
+    bool IsBufferConsumed(const u64 tag) override {
+        if (released_buffer.tag == 0) {
+            if (!released_buffers.try_dequeue(released_buffer)) {
+                return false;
+            }
+        }
+
+        if (released_buffer.tag == tag) {
+            released_buffer.tag = 0;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Empty out the buffer queue.
+     */
+    void ClearQueue() override {
+        samples_buffer.Pop();
+        while (queue.pop()) {
+        }
+        while (released_buffers.pop()) {
+        }
+        queued_buffers = 0;
+        released_buffer = {};
+        playing_buffer = {};
+        playing_buffer.consumed = true;
+    }
+
+private:
+    /**
+     * Signal events back to the audio system that a buffer was played/can be filled.
+     *
+     * @param buffer - Consumed audio buffer to be released.
+     */
+    void SignalEvent(const ::AudioCore::Sink::SinkBuffer& buffer) {
+        auto& manager{system.AudioCore().GetAudioManager()};
+        switch (type) {
+        case StreamType::Out:
+            released_buffers.enqueue(buffer);
+            manager.SetEvent(Event::Type::AudioOutManager, true);
+            break;
+        case StreamType::In:
+            released_buffers.enqueue(buffer);
+            manager.SetEvent(Event::Type::AudioInManager, true);
+            break;
+        case StreamType::Render:
+            break;
+        }
+    }
+
+    /**
+     * Main callback from Cubeb. Either expects samples from us (audio render/audio out), or will
+     * provide samples to be copied (audio in).
+     *
+     * @param stream      - Cubeb-specific data about the stream.
+     * @param user_data   - Custom data pointer passed along, points to a CubebSinkStream.
+     * @param in_buff     - Input buffer to be used if the stream is an input type.
+     * @param out_buff    - Output buffer to be used if the stream is an output type.
+     * @param num_frames_ - Number of frames of audio in the buffers. Note: Not number of samples.
+     */
+    static long DataCallback([[maybe_unused]] cubeb_stream* stream, void* user_data,
+                             [[maybe_unused]] const void* in_buff, void* out_buff,
+                             long num_frames_) {
+        auto* impl = static_cast<CubebSinkStream*>(user_data);
+        if (!impl) {
+            return -1;
+        }
+
+        const std::size_t num_channels = impl->GetDeviceChannels();
+        const std::size_t frame_size = num_channels;
+        const std::size_t frame_size_bytes = frame_size * sizeof(s16);
+        const std::size_t num_frames{static_cast<size_t>(num_frames_)};
+        size_t frames_written{0};
+        [[maybe_unused]] bool underrun{false};
+
+        if (impl->type == StreamType::In) {
+            // INPUT
+            std::span<const s16> input_buffer{reinterpret_cast<const s16*>(in_buff),
+                                              num_frames * frame_size};
+
+            while (frames_written < num_frames) {
+                auto& playing_buffer{impl->playing_buffer};
+
+                // If the playing buffer has been consumed or has no frames, we need a new one
+                if (playing_buffer.consumed || playing_buffer.frames == 0) {
+                    if (!impl->queue.try_dequeue(impl->playing_buffer)) {
+                        // If no buffer was available we've underrun, just push the samples and
+                        // continue.
+                        underrun = true;
+                        impl->samples_buffer.Push(&input_buffer[frames_written * frame_size],
+                                                  (num_frames - frames_written) * frame_size);
+                        frames_written = num_frames;
+                        continue;
+                    } else {
+                        // Successfully got a new buffer, mark the old one as consumed and signal.
+                        impl->queued_buffers--;
+                        impl->SignalEvent(impl->playing_buffer);
+                    }
+                }
+
+                // Get the minimum frames available between the currently playing buffer, and the
+                // amount we have left to fill
+                size_t frames_available{
+                    std::min(playing_buffer.frames - playing_buffer.frames_played,
+                             num_frames - frames_written)};
+
+                impl->samples_buffer.Push(&input_buffer[frames_written * frame_size],
+                                          frames_available * frame_size);
+
+                frames_written += frames_available;
+                playing_buffer.frames_played += frames_available;
+
+                // If that's all the frames in the current buffer, add its samples and mark it as
+                // consumed
+                if (playing_buffer.frames_played >= playing_buffer.frames) {
+                    impl->AddPlayedSampleCount(playing_buffer.frames_played * num_channels);
+                    impl->playing_buffer.consumed = true;
+                }
+            }
+
+            std::memcpy(&impl->last_frame[0], &input_buffer[(frames_written - 1) * frame_size],
+                        frame_size_bytes);
+        } else {
+            // OUTPUT
+            std::span<s16> output_buffer{reinterpret_cast<s16*>(out_buff), num_frames * frame_size};
+
+            while (frames_written < num_frames) {
+                auto& playing_buffer{impl->playing_buffer};
+
+                // If the playing buffer has been consumed or has no frames, we need a new one
+                if (playing_buffer.consumed || playing_buffer.frames == 0) {
+                    if (!impl->queue.try_dequeue(impl->playing_buffer)) {
+                        // If no buffer was available we've underrun, fill the remaining buffer with
+                        // the last written frame and continue.
+                        underrun = true;
+                        for (size_t i = frames_written; i < num_frames; i++) {
+                            std::memcpy(&output_buffer[i * frame_size], &impl->last_frame[0],
+                                        frame_size_bytes);
+                        }
+                        frames_written = num_frames;
+                        continue;
+                    } else {
+                        // Successfully got a new buffer, mark the old one as consumed and signal.
+                        impl->queued_buffers--;
+                        impl->SignalEvent(impl->playing_buffer);
+                    }
+                }
+
+                // Get the minimum frames available between the currently playing buffer, and the
+                // amount we have left to fill
+                size_t frames_available{
+                    std::min(playing_buffer.frames - playing_buffer.frames_played,
+                             num_frames - frames_written)};
+
+                impl->samples_buffer.Pop(&output_buffer[frames_written * frame_size],
+                                         frames_available * frame_size);
+
+                frames_written += frames_available;
+                playing_buffer.frames_played += frames_available;
+
+                // If that's all the frames in the current buffer, add its samples and mark it as
+                // consumed
+                if (playing_buffer.frames_played >= playing_buffer.frames) {
+                    impl->AddPlayedSampleCount(playing_buffer.frames_played * num_channels);
+                    impl->playing_buffer.consumed = true;
+                }
+            }
+
+            std::memcpy(&impl->last_frame[0], &output_buffer[(frames_written - 1) * frame_size],
+                        frame_size_bytes);
+        }
+
+        return num_frames_;
+    }
+
+    /**
+     * Cubeb callback for if a device state changes. Unused currently.
+     *
+     * @param stream      - Cubeb-specific data about the stream.
+     * @param user_data   - Custom data pointer passed along, points to a CubebSinkStream.
+     * @param state       - New state of the device.
+     */
+    static void StateCallback([[maybe_unused]] cubeb_stream* stream,
+                              [[maybe_unused]] void* user_data,
+                              [[maybe_unused]] cubeb_state state) {}
+
+    /// Main Cubeb context
+    cubeb* ctx{};
+    /// Cubeb stream backend
+    cubeb_stream* stream_backend{};
+    /// Name of this stream
+    std::string name{};
+    /// Type of this stream
+    StreamType type;
+    /// Core system
+    Core::System& system;
+    /// Ring buffer of the samples waiting to be played or consumed
+    Common::RingBuffer<s16, 0x10000> samples_buffer;
+    /// Audio buffers queued and waiting to play
+    Common::ReaderWriterQueue<::AudioCore::Sink::SinkBuffer> queue;
+    /// The currently-playing audio buffer
+    ::AudioCore::Sink::SinkBuffer playing_buffer{};
+    /// Audio buffers which have been played and are in queue to be released by the audio system
+    Common::ReaderWriterQueue<::AudioCore::Sink::SinkBuffer> released_buffers{};
+    /// Currently released buffer waiting to be taken by the audio system
+    ::AudioCore::Sink::SinkBuffer released_buffer{};
+    /// The last played (or received) frame of audio, used when the callback underruns
+    std::array<s16, MaxChannels> last_frame{};
+};
+
+CubebSink::CubebSink(std::string_view target_device_name) {
+    // Cubeb requires COM to be initialized on the thread calling cubeb_init on Windows
+#ifdef _WIN32
+    com_init_result = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
+#endif
+
+    if (cubeb_init(&ctx, "yuzu", nullptr) != CUBEB_OK) {
+        LOG_CRITICAL(Audio_Sink, "cubeb_init failed");
+        return;
+    }
+
+    if (target_device_name != auto_device_name && !target_device_name.empty()) {
+        cubeb_device_collection collection;
+        if (cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &collection) != CUBEB_OK) {
+            LOG_WARNING(Audio_Sink, "Audio output device enumeration not supported");
+        } else {
+            const auto collection_end{collection.device + collection.count};
+            const auto device{
+                std::find_if(collection.device, collection_end, [&](const cubeb_device_info& info) {
+                    return info.friendly_name != nullptr &&
+                           target_device_name == std::string(info.friendly_name);
+                })};
+            if (device != collection_end) {
+                output_device = device->devid;
+            }
+            cubeb_device_collection_destroy(ctx, &collection);
+        }
+    }
+
+    cubeb_get_max_channel_count(ctx, &device_channels);
+    device_channels = device_channels >= 6U ? 6U : 2U;
+}
+
+CubebSink::~CubebSink() {
+    if (!ctx) {
+        return;
+    }
+
+    for (auto& sink_stream : sink_streams) {
+        sink_stream.reset();
+    }
+
+    cubeb_destroy(ctx);
+
+#ifdef _WIN32
+    if (SUCCEEDED(com_init_result)) {
+        CoUninitialize();
+    }
+#endif
+}
+
+SinkStream* CubebSink::AcquireSinkStream(Core::System& system, const u32 system_channels,
+                                         const std::string& name, const StreamType type) {
+    SinkStreamPtr& stream = sink_streams.emplace_back(std::make_unique<CubebSinkStream>(
+        ctx, device_channels, system_channels, output_device, input_device, name, type, system));
+
+    return stream.get();
+}
+
+void CubebSink::CloseStream(const SinkStream* stream) {
+    for (size_t i = 0; i < sink_streams.size(); i++) {
+        if (sink_streams[i].get() == stream) {
+            sink_streams[i].reset();
+            sink_streams.erase(sink_streams.begin() + i);
+            break;
+        }
+    }
+}
+
+void CubebSink::CloseStreams() {
+    sink_streams.clear();
+}
+
+void CubebSink::PauseStreams() {
+    for (auto& stream : sink_streams) {
+        stream->Stop();
+    }
+}
+
+void CubebSink::UnpauseStreams() {
+    for (auto& stream : sink_streams) {
+        stream->Start(true);
+    }
+}
+
+f32 CubebSink::GetDeviceVolume() const {
+    if (sink_streams.empty()) {
+        return 1.0f;
+    }
+
+    return sink_streams[0]->GetDeviceVolume();
+}
+
+void CubebSink::SetDeviceVolume(const f32 volume) {
+    for (auto& stream : sink_streams) {
+        stream->SetDeviceVolume(volume);
+    }
+}
+
+void CubebSink::SetSystemVolume(const f32 volume) {
+    for (auto& stream : sink_streams) {
+        stream->SetSystemVolume(volume);
+    }
+}
+
+std::vector<std::string> ListCubebSinkDevices(const bool capture) {
+    std::vector<std::string> device_list;
+    cubeb* ctx;
+
+    if (cubeb_init(&ctx, "yuzu Device Enumerator", nullptr) != CUBEB_OK) {
+        LOG_CRITICAL(Audio_Sink, "cubeb_init failed");
+        return {};
+    }
+
+    auto type{capture ? CUBEB_DEVICE_TYPE_INPUT : CUBEB_DEVICE_TYPE_OUTPUT};
+    cubeb_device_collection collection;
+    if (cubeb_enumerate_devices(ctx, type, &collection) != CUBEB_OK) {
+        LOG_WARNING(Audio_Sink, "Audio output device enumeration not supported");
+    } else {
+        for (std::size_t i = 0; i < collection.count; i++) {
+            const cubeb_device_info& device = collection.device[i];
+            if (device.friendly_name && device.friendly_name[0] != '\0' &&
+                device.state == CUBEB_DEVICE_STATE_ENABLED) {
+                device_list.emplace_back(device.friendly_name);
+            }
+        }
+        cubeb_device_collection_destroy(ctx, &collection);
+    }
+
+    cubeb_destroy(ctx);
+    return device_list;
+}
+
+} // namespace AudioCore::Sink
diff --git a/src/audio_core/sink/cubeb_sink.h b/src/audio_core/sink/cubeb_sink.h
new file mode 100644
index 0000000000..f0f43dfa12
--- /dev/null
+++ b/src/audio_core/sink/cubeb_sink.h
@@ -0,0 +1,110 @@
+// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include <cubeb/cubeb.h>
+
+#include "audio_core/sink/sink.h"
+
+namespace Core {
+class System;
+}
+
+namespace AudioCore::Sink {
+class SinkStream;
+
+/**
+ * Cubeb backend sink, holds multiple output streams and is responsible for sinking samples to
+ * hardware. Used by Audio Render, Audio In and Audio Out.
+ */
+class CubebSink final : public Sink {
+public:
+    explicit CubebSink(std::string_view device_id);
+    ~CubebSink() override;
+
+    /**
+     * Create a new sink stream.
+     *
+     * @param system          - Core system.
+     * @param system_channels - Number of channels the audio system expects.
+     *                          May differ from the device's channel count.
+     * @param name            - Name of this stream.
+     * @param type            - Type of this stream, render/in/out.
+     * @param event           - Audio render only, a signal used to prevent the renderer running too
+     *                          fast.
+     * @return A pointer to the created SinkStream
+     */
+    SinkStream* AcquireSinkStream(Core::System& system, u32 system_channels,
+                                  const std::string& name, StreamType type) override;
+
+    /**
+     * Close a given stream.
+     *
+     * @param stream - The stream to close.
+     */
+    void CloseStream(const SinkStream* stream) override;
+
+    /**
+     * Close all streams.
+     */
+    void CloseStreams() override;
+
+    /**
+     * Pause all streams.
+     */
+    void PauseStreams() override;
+
+    /**
+     * Unpause all streams.
+     */
+    void UnpauseStreams() override;
+
+    /**
+     * Get the device volume. Set from calls to the IAudioDevice service.
+     *
+     * @return Volume of the device.
+     */
+    f32 GetDeviceVolume() const override;
+
+    /**
+     * Set the device volume. Set from calls to the IAudioDevice service.
+     *
+     * @param volume - New volume of the device.
+     */
+    void SetDeviceVolume(f32 volume) override;
+
+    /**
+     * Set the system volume. Comes from the audio system using this stream.
+     *
+     * @param volume - New volume of the system.
+     */
+    void SetSystemVolume(f32 volume) override;
+
+private:
+    /// Backend Cubeb context
+    cubeb* ctx{};
+    /// Cubeb id of the actual hardware output device
+    cubeb_devid output_device{};
+    /// Cubeb id of the actual hardware input device
+    cubeb_devid input_device{};
+    /// Vector of streams managed by this sink
+    std::vector<SinkStreamPtr> sink_streams{};
+
+#ifdef _WIN32
+    /// Cubeb required COM to be initialized multi-threaded on Windows
+    u32 com_init_result = 0;
+#endif
+};
+
+/**
+ * Get a list of conencted devices from Cubeb.
+ *
+ * @param capture - Return input (capture) devices if true, otherwise output devices.
+ */
+std::vector<std::string> ListCubebSinkDevices(bool capture);
+
+} // namespace AudioCore::Sink
diff --git a/src/audio_core/sink/null_sink.h b/src/audio_core/sink/null_sink.h
new file mode 100644
index 0000000000..47a3421719
--- /dev/null
+++ b/src/audio_core/sink/null_sink.h
@@ -0,0 +1,52 @@
+// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "audio_core/sink/sink.h"
+#include "audio_core/sink/sink_stream.h"
+
+namespace AudioCore::Sink {
+/**
+ * A no-op sink for when no audio out is wanted.
+ */
+class NullSink final : public Sink {
+public:
+    explicit NullSink(std::string_view) {}
+    ~NullSink() override = default;
+
+    SinkStream* AcquireSinkStream([[maybe_unused]] Core::System& system,
+                                  [[maybe_unused]] u32 system_channels,
+                                  [[maybe_unused]] const std::string& name,
+                                  [[maybe_unused]] StreamType type) override {
+        return &null_sink_stream;
+    }
+
+    void CloseStream([[maybe_unused]] const SinkStream* stream) override {}
+    void CloseStreams() override {}
+    void PauseStreams() override {}
+    void UnpauseStreams() override {}
+    f32 GetDeviceVolume() const override {
+        return 1.0f;
+    }
+    void SetDeviceVolume(f32 volume) override {}
+    void SetSystemVolume(f32 volume) override {}
+
+private:
+    struct NullSinkStreamImpl final : SinkStream {
+        void Finalize() override {}
+        void Start(bool resume = false) override {}
+        void Stop() override {}
+        void AppendBuffer([[maybe_unused]] ::AudioCore::Sink::SinkBuffer& buffer,
+                          [[maybe_unused]] std::vector<s16>& samples) override {}
+        std::vector<s16> ReleaseBuffer([[maybe_unused]] u64 num_samples) override {
+            return {};
+        }
+        bool IsBufferConsumed([[maybe_unused]] const u64 tag) {
+            return true;
+        }
+        void ClearQueue() override {}
+    } null_sink_stream;
+};
+
+} // namespace AudioCore::Sink
diff --git a/src/audio_core/sink/sdl2_sink.cpp b/src/audio_core/sink/sdl2_sink.cpp
new file mode 100644
index 0000000000..d6c9ec90dd
--- /dev/null
+++ b/src/audio_core/sink/sdl2_sink.cpp
@@ -0,0 +1,556 @@
+// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <algorithm>
+#include <atomic>
+
+#include "audio_core/audio_core.h"
+#include "audio_core/audio_event.h"
+#include "audio_core/audio_manager.h"
+#include "audio_core/sink/sdl2_sink.h"
+#include "audio_core/sink/sink_stream.h"
+#include "common/assert.h"
+#include "common/fixed_point.h"
+#include "common/logging/log.h"
+#include "common/reader_writer_queue.h"
+#include "common/ring_buffer.h"
+#include "common/settings.h"
+#include "core/core.h"
+
+// Ignore -Wimplicit-fallthrough due to https://github.com/libsdl-org/SDL/issues/4307
+#ifdef __clang__
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wimplicit-fallthrough"
+#endif
+#include <SDL.h>
+#ifdef __clang__
+#pragma clang diagnostic pop
+#endif
+
+namespace AudioCore::Sink {
+/**
+ * SDL sink stream, responsible for sinking samples to hardware.
+ */
+class SDLSinkStream final : public SinkStream {
+public:
+    /**
+     * Create a new sink stream.
+     *
+     * @param device_channels_ - Number of channels supported by the hardware.
+     * @param system_channels_ - Number of channels the audio systems expect.
+     * @param output_device    - Name of the output device to use for this stream.
+     * @param input_device     - Name of the input device to use for this stream.
+     * @param type_            - Type of this stream.
+     * @param system_          - Core system.
+     * @param event            - Event used only for audio renderer, signalled on buffer consume.
+     */
+    SDLSinkStream(u32 device_channels_, const u32 system_channels_,
+                  const std::string& output_device, const std::string& input_device,
+                  const StreamType type_, Core::System& system_)
+        : type{type_}, system{system_} {
+        system_channels = system_channels_;
+        device_channels = device_channels_;
+
+        SDL_AudioSpec spec;
+        spec.freq = TargetSampleRate;
+        spec.channels = static_cast<u8>(device_channels);
+        spec.format = AUDIO_S16SYS;
+        if (type == StreamType::Render) {
+            spec.samples = TargetSampleCount;
+        } else {
+            spec.samples = 1024;
+        }
+        spec.callback = &SDLSinkStream::DataCallback;
+        spec.userdata = this;
+
+        playing_buffer.consumed = true;
+
+        std::string device_name{output_device};
+        bool capture{false};
+        if (type == StreamType::In) {
+            device_name = input_device;
+            capture = true;
+        }
+
+        SDL_AudioSpec obtained;
+        if (device_name.empty()) {
+            device = SDL_OpenAudioDevice(nullptr, capture, &spec, &obtained, false);
+        } else {
+            device = SDL_OpenAudioDevice(device_name.c_str(), capture, &spec, &obtained, false);
+        }
+
+        if (device == 0) {
+            LOG_CRITICAL(Audio_Sink, "Error opening SDL audio device: {}", SDL_GetError());
+            return;
+        }
+
+        LOG_DEBUG(Service_Audio,
+                  "Opening sdl stream {} with: rate {} channels {} (system channels {}) "
+                  " samples {}",
+                  device, obtained.freq, obtained.channels, system_channels, obtained.samples);
+    }
+
+    /**
+     * Destroy the sink stream.
+     */
+    ~SDLSinkStream() override {
+        if (device == 0) {
+            return;
+        }
+
+        SDL_CloseAudioDevice(device);
+    }
+
+    /**
+     * Finalize the sink stream.
+     */
+    void Finalize() override {
+        if (device == 0) {
+            return;
+        }
+
+        SDL_CloseAudioDevice(device);
+    }
+
+    /**
+     * Start the sink stream.
+     *
+     * @param resume - Set to true if this is resuming the stream a previously-active stream.
+     *                 Default false.
+     */
+    void Start(const bool resume = false) override {
+        if (device == 0) {
+            return;
+        }
+
+        if (resume && was_playing) {
+            SDL_PauseAudioDevice(device, 0);
+            paused = false;
+        } else if (!resume) {
+            SDL_PauseAudioDevice(device, 0);
+            paused = false;
+        }
+    }
+
+    /**
+     * Stop the sink stream.
+     */
+    void Stop() {
+        if (device == 0) {
+            return;
+        }
+        SDL_PauseAudioDevice(device, 1);
+        paused = true;
+    }
+
+    /**
+     * Append a new buffer and its samples to a waiting queue to play.
+     *
+     * @param buffer  - Audio buffer information to be queued.
+     * @param samples - The s16 samples to be queue for playback.
+     */
+    void AppendBuffer(::AudioCore::Sink::SinkBuffer& buffer, std::vector<s16>& samples) override {
+        if (type == StreamType::In) {
+            queue.enqueue(buffer);
+            queued_buffers++;
+        } else {
+            constexpr s32 min = std::numeric_limits<s16>::min();
+            constexpr s32 max = std::numeric_limits<s16>::max();
+
+            auto yuzu_volume{Settings::Volume()};
+            auto volume{system_volume * device_volume * yuzu_volume};
+
+            if (system_channels == 6 && device_channels == 2) {
+                // We're given 6 channels, but our device only outputs 2, so downmix.
+                constexpr std::array<f32, 4> down_mix_coeff{1.0f, 0.707f, 0.251f, 0.707f};
+
+                for (u32 read_index = 0, write_index = 0; read_index < samples.size();
+                     read_index += system_channels, write_index += device_channels) {
+                    const auto left_sample{
+                        ((Common::FixedPoint<49, 15>(
+                              samples[read_index + static_cast<u32>(Channels::FrontLeft)]) *
+                              down_mix_coeff[0] +
+                          samples[read_index + static_cast<u32>(Channels::Center)] *
+                              down_mix_coeff[1] +
+                          samples[read_index + static_cast<u32>(Channels::LFE)] *
+                              down_mix_coeff[2] +
+                          samples[read_index + static_cast<u32>(Channels::BackLeft)] *
+                              down_mix_coeff[3]) *
+                         volume)
+                            .to_int()};
+
+                    const auto right_sample{
+                        ((Common::FixedPoint<49, 15>(
+                              samples[read_index + static_cast<u32>(Channels::FrontRight)]) *
+                              down_mix_coeff[0] +
+                          samples[read_index + static_cast<u32>(Channels::Center)] *
+                              down_mix_coeff[1] +
+                          samples[read_index + static_cast<u32>(Channels::LFE)] *
+                              down_mix_coeff[2] +
+                          samples[read_index + static_cast<u32>(Channels::BackRight)] *
+                              down_mix_coeff[3]) *
+                         volume)
+                            .to_int()};
+
+                    samples[write_index + static_cast<u32>(Channels::FrontLeft)] =
+                        static_cast<s16>(std::clamp(left_sample, min, max));
+                    samples[write_index + static_cast<u32>(Channels::FrontRight)] =
+                        static_cast<s16>(std::clamp(right_sample, min, max));
+                }
+
+                samples.resize(samples.size() / system_channels * device_channels);
+
+            } else if (system_channels == 2 && device_channels == 6) {
+                // We need moar samples! Not all games will provide 6 channel audio.
+                // TODO: Implement some upmixing here. Currently just passthrough, with other
+                // channels left as silence.
+                std::vector<s16> new_samples(samples.size() / system_channels * device_channels, 0);
+
+                for (u32 read_index = 0, write_index = 0; read_index < samples.size();
+                     read_index += system_channels, write_index += device_channels) {
+                    const auto left_sample{static_cast<s16>(std::clamp(
+                        static_cast<s32>(
+                            static_cast<f32>(
+                                samples[read_index + static_cast<u32>(Channels::FrontLeft)]) *
+                            volume),
+                        min, max))};
+
+                    new_samples[write_index + static_cast<u32>(Channels::FrontLeft)] = left_sample;
+
+                    const auto right_sample{static_cast<s16>(std::clamp(
+                        static_cast<s32>(
+                            static_cast<f32>(
+                                samples[read_index + static_cast<u32>(Channels::FrontRight)]) *
+                            volume),
+                        min, max))};
+
+                    new_samples[write_index + static_cast<u32>(Channels::FrontRight)] =
+                        right_sample;
+                }
+                samples = std::move(new_samples);
+
+            } else if (volume != 1.0f) {
+                for (u32 i = 0; i < samples.size(); i++) {
+                    samples[i] = static_cast<s16>(std::clamp(
+                        static_cast<s32>(static_cast<f32>(samples[i]) * volume), min, max));
+                }
+            }
+
+            samples_buffer.Push(samples);
+            queue.enqueue(buffer);
+            queued_buffers++;
+        }
+    }
+
+    /**
+     * Release a buffer. Audio In only, will fill a buffer with recorded samples.
+     *
+     * @param num_samples - Maximum number of samples to receive.
+     * @return Vector of recorded samples. May have fewer than num_samples.
+     */
+    std::vector<s16> ReleaseBuffer(const u64 num_samples) override {
+        static constexpr s32 min = std::numeric_limits<s16>::min();
+        static constexpr s32 max = std::numeric_limits<s16>::max();
+
+        auto samples{samples_buffer.Pop(num_samples)};
+
+        // TODO: Up-mix to 6 channels if the game expects it.
+        // For audio input this is unlikely to ever be the case though.
+
+        // Incoming mic volume seems to always be very quiet, so multiply by an additional 8 here.
+        // TODO: Play with this and find something that works better.
+        auto volume{system_volume * device_volume * 8};
+        for (u32 i = 0; i < samples.size(); i++) {
+            samples[i] = static_cast<s16>(
+                std::clamp(static_cast<s32>(static_cast<f32>(samples[i]) * volume), min, max));
+        }
+
+        if (samples.size() < num_samples) {
+            samples.resize(num_samples, 0);
+        }
+        return samples;
+    }
+
+    /**
+     * Check if a certain buffer has been consumed (fully played).
+     *
+     * @param tag - Unique tag of a buffer to check for.
+     * @return True if the buffer has been played, otherwise false.
+     */
+    bool IsBufferConsumed(const u64 tag) override {
+        if (released_buffer.tag == 0) {
+            if (!released_buffers.try_dequeue(released_buffer)) {
+                return false;
+            }
+        }
+
+        if (released_buffer.tag == tag) {
+            released_buffer.tag = 0;
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Empty out the buffer queue.
+     */
+    void ClearQueue() override {
+        samples_buffer.Pop();
+        while (queue.pop()) {
+        }
+        while (released_buffers.pop()) {
+        }
+        released_buffer = {};
+        playing_buffer = {};
+        playing_buffer.consumed = true;
+        queued_buffers = 0;
+    }
+
+private:
+    /**
+     * Signal events back to the audio system that a buffer was played/can be filled.
+     *
+     * @param buffer - Consumed audio buffer to be released.
+     */
+    void SignalEvent(const ::AudioCore::Sink::SinkBuffer& buffer) {
+        auto& manager{system.AudioCore().GetAudioManager()};
+        switch (type) {
+        case StreamType::Out:
+            released_buffers.enqueue(buffer);
+            manager.SetEvent(Event::Type::AudioOutManager, true);
+            break;
+        case StreamType::In:
+            released_buffers.enqueue(buffer);
+            manager.SetEvent(Event::Type::AudioInManager, true);
+            break;
+        case StreamType::Render:
+            break;
+        }
+    }
+
+    /**
+     * Main callback from SDL. Either expects samples from us (audio render/audio out), or will
+     * provide samples to be copied (audio in).
+     *
+     * @param userdata - Custom data pointer passed along, points to a SDLSinkStream.
+     * @param stream   - Buffer of samples to be filled or read.
+     * @param len      - Length of the stream in bytes.
+     */
+    static void DataCallback(void* userdata, Uint8* stream, int len) {
+        auto* impl = static_cast<SDLSinkStream*>(userdata);
+
+        if (!impl) {
+            return;
+        }
+
+        const std::size_t num_channels = impl->GetDeviceChannels();
+        const std::size_t frame_size = num_channels;
+        const std::size_t frame_size_bytes = frame_size * sizeof(s16);
+        const std::size_t num_frames{len / num_channels / sizeof(s16)};
+        size_t frames_written{0};
+        [[maybe_unused]] bool underrun{false};
+
+        if (impl->type == StreamType::In) {
+            std::span<s16> input_buffer{reinterpret_cast<s16*>(stream), num_frames * frame_size};
+
+            while (frames_written < num_frames) {
+                auto& playing_buffer{impl->playing_buffer};
+
+                // If the playing buffer has been consumed or has no frames, we need a new one
+                if (playing_buffer.consumed || playing_buffer.frames == 0) {
+                    if (!impl->queue.try_dequeue(impl->playing_buffer)) {
+                        // If no buffer was available we've underrun, just push the samples and
+                        // continue.
+                        underrun = true;
+                        impl->samples_buffer.Push(&input_buffer[frames_written * frame_size],
+                                                  (num_frames - frames_written) * frame_size);
+                        frames_written = num_frames;
+                        continue;
+                    } else {
+                        impl->queued_buffers--;
+                        impl->SignalEvent(impl->playing_buffer);
+                    }
+                }
+
+                // Get the minimum frames available between the currently playing buffer, and the
+                // amount we have left to fill
+                size_t frames_available{
+                    std::min(playing_buffer.frames - playing_buffer.frames_played,
+                             num_frames - frames_written)};
+
+                impl->samples_buffer.Push(&input_buffer[frames_written * frame_size],
+                                          frames_available * frame_size);
+
+                frames_written += frames_available;
+                playing_buffer.frames_played += frames_available;
+
+                // If that's all the frames in the current buffer, add its samples and mark it as
+                // consumed
+                if (playing_buffer.frames_played >= playing_buffer.frames) {
+                    impl->AddPlayedSampleCount(playing_buffer.frames_played * num_channels);
+                    impl->playing_buffer.consumed = true;
+                }
+            }
+
+            std::memcpy(&impl->last_frame[0], &input_buffer[(frames_written - 1) * frame_size],
+                        frame_size_bytes);
+        } else {
+            std::span<s16> output_buffer{reinterpret_cast<s16*>(stream), num_frames * frame_size};
+
+            while (frames_written < num_frames) {
+                auto& playing_buffer{impl->playing_buffer};
+
+                // If the playing buffer has been consumed or has no frames, we need a new one
+                if (playing_buffer.consumed || playing_buffer.frames == 0) {
+                    if (!impl->queue.try_dequeue(impl->playing_buffer)) {
+                        // If no buffer was available we've underrun, fill the remaining buffer with
+                        // the last written frame and continue.
+                        underrun = true;
+                        for (size_t i = frames_written; i < num_frames; i++) {
+                            std::memcpy(&output_buffer[i * frame_size], &impl->last_frame[0],
+                                        frame_size_bytes);
+                        }
+                        frames_written = num_frames;
+                        continue;
+                    } else {
+                        impl->queued_buffers--;
+                        impl->SignalEvent(impl->playing_buffer);
+                    }
+                }
+
+                // Get the minimum frames available between the currently playing buffer, and the
+                // amount we have left to fill
+                size_t frames_available{
+                    std::min(playing_buffer.frames - playing_buffer.frames_played,
+                             num_frames - frames_written)};
+
+                impl->samples_buffer.Pop(&output_buffer[frames_written * frame_size],
+                                         frames_available * frame_size);
+
+                frames_written += frames_available;
+                playing_buffer.frames_played += frames_available;
+
+                // If that's all the frames in the current buffer, add its samples and mark it as
+                // consumed
+                if (playing_buffer.frames_played >= playing_buffer.frames) {
+                    impl->AddPlayedSampleCount(playing_buffer.frames_played * num_channels);
+                    impl->playing_buffer.consumed = true;
+                }
+            }
+
+            std::memcpy(&impl->last_frame[0], &output_buffer[(frames_written - 1) * frame_size],
+                        frame_size_bytes);
+        }
+    }
+
+    /// SDL device id of the opened input/output device
+    SDL_AudioDeviceID device{};
+    /// Type of this stream
+    StreamType type;
+    /// Core system
+    Core::System& system;
+    /// Ring buffer of the samples waiting to be played or consumed
+    Common::RingBuffer<s16, 0x10000> samples_buffer;
+    /// Audio buffers queued and waiting to play
+    Common::ReaderWriterQueue<::AudioCore::Sink::SinkBuffer> queue;
+    /// The currently-playing audio buffer
+    ::AudioCore::Sink::SinkBuffer playing_buffer{};
+    /// Audio buffers which have been played and are in queue to be released by the audio system
+    Common::ReaderWriterQueue<::AudioCore::Sink::SinkBuffer> released_buffers{};
+    /// Currently released buffer waiting to be taken by the audio system
+    ::AudioCore::Sink::SinkBuffer released_buffer{};
+    /// The last played (or received) frame of audio, used when the callback underruns
+    std::array<s16, MaxChannels> last_frame{};
+};
+
+SDLSink::SDLSink(std::string_view target_device_name) {
+    if (!SDL_WasInit(SDL_INIT_AUDIO)) {
+        if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
+            LOG_CRITICAL(Audio_Sink, "SDL_InitSubSystem audio failed: {}", SDL_GetError());
+            return;
+        }
+    }
+
+    if (target_device_name != auto_device_name && !target_device_name.empty()) {
+        output_device = target_device_name;
+    } else {
+        output_device.clear();
+    }
+
+    device_channels = 2;
+}
+
+SDLSink::~SDLSink() = default;
+
+SinkStream* SDLSink::AcquireSinkStream(Core::System& system, const u32 system_channels,
+                                       const std::string&, const StreamType type) {
+    SinkStreamPtr& stream = sink_streams.emplace_back(std::make_unique<SDLSinkStream>(
+        device_channels, system_channels, output_device, input_device, type, system));
+    return stream.get();
+}
+
+void SDLSink::CloseStream(const SinkStream* stream) {
+    for (size_t i = 0; i < sink_streams.size(); i++) {
+        if (sink_streams[i].get() == stream) {
+            sink_streams[i].reset();
+            sink_streams.erase(sink_streams.begin() + i);
+            break;
+        }
+    }
+}
+
+void SDLSink::CloseStreams() {
+    sink_streams.clear();
+}
+
+void SDLSink::PauseStreams() {
+    for (auto& stream : sink_streams) {
+        stream->Stop();
+    }
+}
+
+void SDLSink::UnpauseStreams() {
+    for (auto& stream : sink_streams) {
+        stream->Start();
+    }
+}
+
+f32 SDLSink::GetDeviceVolume() const {
+    if (sink_streams.empty()) {
+        return 1.0f;
+    }
+
+    return sink_streams[0]->GetDeviceVolume();
+}
+
+void SDLSink::SetDeviceVolume(const f32 volume) {
+    for (auto& stream : sink_streams) {
+        stream->SetDeviceVolume(volume);
+    }
+}
+
+void SDLSink::SetSystemVolume(const f32 volume) {
+    for (auto& stream : sink_streams) {
+        stream->SetSystemVolume(volume);
+    }
+}
+
+std::vector<std::string> ListSDLSinkDevices(const bool capture) {
+    std::vector<std::string> device_list;
+
+    if (!SDL_WasInit(SDL_INIT_AUDIO)) {
+        if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
+            LOG_CRITICAL(Audio_Sink, "SDL_InitSubSystem audio failed: {}", SDL_GetError());
+            return {};
+        }
+    }
+
+    const int device_count = SDL_GetNumAudioDevices(capture);
+    for (int i = 0; i < device_count; ++i) {
+        device_list.emplace_back(SDL_GetAudioDeviceName(i, 0));
+    }
+
+    return device_list;
+}
+
+} // namespace AudioCore::Sink
diff --git a/src/audio_core/sink/sdl2_sink.h b/src/audio_core/sink/sdl2_sink.h
new file mode 100644
index 0000000000..186bc2fa3a
--- /dev/null
+++ b/src/audio_core/sink/sdl2_sink.h
@@ -0,0 +1,101 @@
+// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include "audio_core/sink/sink.h"
+
+namespace Core {
+class System;
+}
+
+namespace AudioCore::Sink {
+class SinkStream;
+
+/**
+ * SDL backend sink, holds multiple output streams and is responsible for sinking samples to
+ * hardware. Used by Audio Render, Audio In and Audio Out.
+ */
+class SDLSink final : public Sink {
+public:
+    explicit SDLSink(std::string_view device_id);
+    ~SDLSink() override;
+
+    /**
+     * Create a new sink stream.
+     *
+     * @param system          - Core system.
+     * @param system_channels - Number of channels the audio system expects.
+     *                          May differ from the device's channel count.
+     * @param name            - Name of this stream.
+     * @param type            - Type of this stream, render/in/out.
+     * @param event           - Audio render only, a signal used to prevent the renderer running too
+     *                          fast.
+     * @return A pointer to the created SinkStream
+     */
+    SinkStream* AcquireSinkStream(Core::System& system, u32 system_channels,
+                                  const std::string& name, StreamType type) override;
+
+    /**
+     * Close a given stream.
+     *
+     * @param stream - The stream to close.
+     */
+    void CloseStream(const SinkStream* stream) override;
+
+    /**
+     * Close all streams.
+     */
+    void CloseStreams() override;
+
+    /**
+     * Pause all streams.
+     */
+    void PauseStreams() override;
+
+    /**
+     * Unpause all streams.
+     */
+    void UnpauseStreams() override;
+
+    /**
+     * Get the device volume. Set from calls to the IAudioDevice service.
+     *
+     * @return Volume of the device.
+     */
+    f32 GetDeviceVolume() const override;
+
+    /**
+     * Set the device volume. Set from calls to the IAudioDevice service.
+     *
+     * @param volume - New volume of the device.
+     */
+    void SetDeviceVolume(f32 volume) override;
+
+    /**
+     * Set the system volume. Comes from the audio system using this stream.
+     *
+     * @param volume - New volume of the system.
+     */
+    void SetSystemVolume(f32 volume) override;
+
+private:
+    /// Name of the output device used by streams
+    std::string output_device;
+    /// Name of the input device used by streams
+    std::string input_device;
+    /// Vector of streams managed by this sink
+    std::vector<SinkStreamPtr> sink_streams;
+};
+
+/**
+ * Get a list of conencted devices from Cubeb.
+ *
+ * @param capture - Return input (capture) devices if true, otherwise output devices.
+ */
+std::vector<std::string> ListSDLSinkDevices(bool capture);
+
+} // namespace AudioCore::Sink
diff --git a/src/audio_core/sink/sink.h b/src/audio_core/sink/sink.h
new file mode 100644
index 0000000000..91fe455e48
--- /dev/null
+++ b/src/audio_core/sink/sink.h
@@ -0,0 +1,106 @@
+// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <memory>
+#include <string>
+
+#include "audio_core/sink/sink_stream.h"
+#include "common/common_types.h"
+
+namespace Common {
+class Event;
+}
+namespace Core {
+class System;
+}
+
+namespace AudioCore::Sink {
+
+constexpr char auto_device_name[] = "auto";
+
+/**
+ * This class is an interface for an audio sink, holds multiple output streams and is responsible
+ * for sinking samples to hardware. Used by Audio Render, Audio In and Audio Out.
+ */
+class Sink {
+public:
+    virtual ~Sink() = default;
+    /**
+     * Close a given stream.
+     *
+     * @param stream - The stream to close.
+     */
+    virtual void CloseStream(const SinkStream* stream) = 0;
+
+    /**
+     * Close all streams.
+     */
+    virtual void CloseStreams() = 0;
+
+    /**
+     * Pause all streams.
+     */
+    virtual void PauseStreams() = 0;
+
+    /**
+     * Unpause all streams.
+     */
+    virtual void UnpauseStreams() = 0;
+
+    /**
+     * Create a new sink stream, kept within this sink, with a pointer returned for use.
+     * Do not free the returned pointer. When done with the stream, call CloseStream on the sink.
+     *
+     * @param system          - Core system.
+     * @param system_channels - Number of channels the audio system expects.
+     *                          May differ from the device's channel count.
+     * @param name            - Name of this stream.
+     * @param type            - Type of this stream, render/in/out.
+     * @param event           - Audio render only, a signal used to prevent the renderer running too
+     *                          fast.
+     * @return A pointer to the created SinkStream
+     */
+    virtual SinkStream* AcquireSinkStream(Core::System& system, u32 system_channels,
+                                          const std::string& name, StreamType type) = 0;
+
+    /**
+     * Get the number of channels the hardware device supports.
+     * Either 2 or 6.
+     *
+     * @return Number of device channels.
+     */
+    u32 GetDeviceChannels() const {
+        return device_channels;
+    }
+
+    /**
+     * Get the device volume. Set from calls to the IAudioDevice service.
+     *
+     * @return Volume of the device.
+     */
+    virtual f32 GetDeviceVolume() const = 0;
+
+    /**
+     * Set the device volume. Set from calls to the IAudioDevice service.
+     *
+     * @param volume - New volume of the device.
+     */
+    virtual void SetDeviceVolume(f32 volume) = 0;
+
+    /**
+     * Set the system volume. Comes from the audio system using this stream.
+     *
+     * @param volume - New volume of the system.
+     */
+    virtual void SetSystemVolume(f32 volume) = 0;
+
+protected:
+    /// Number of device channels supported by the hardware
+    u32 device_channels{2};
+};
+
+using SinkPtr = std::unique_ptr<Sink>;
+
+} // namespace AudioCore::Sink
diff --git a/src/audio_core/sink_details.cpp b/src/audio_core/sink/sink_details.cpp
similarity index 76%
rename from src/audio_core/sink_details.cpp
rename to src/audio_core/sink/sink_details.cpp
index c4cc661119..253c0fd1e5 100644
--- a/src/audio_core/sink_details.cpp
+++ b/src/audio_core/sink/sink_details.cpp
@@ -5,21 +5,21 @@
 #include <memory>
 #include <string>
 #include <vector>
-#include "audio_core/null_sink.h"
-#include "audio_core/sink_details.h"
+#include "audio_core/sink/null_sink.h"
+#include "audio_core/sink/sink_details.h"
 #ifdef HAVE_CUBEB
-#include "audio_core/cubeb_sink.h"
+#include "audio_core/sink/cubeb_sink.h"
 #endif
 #ifdef HAVE_SDL2
-#include "audio_core/sdl2_sink.h"
+#include "audio_core/sink/sdl2_sink.h"
 #endif
 #include "common/logging/log.h"
 
-namespace AudioCore {
+namespace AudioCore::Sink {
 namespace {
 struct SinkDetails {
     using FactoryFn = std::unique_ptr<Sink> (*)(std::string_view);
-    using ListDevicesFn = std::vector<std::string> (*)();
+    using ListDevicesFn = std::vector<std::string> (*)(bool);
 
     /// Name for this sink.
     const char* id;
@@ -49,17 +49,18 @@ constexpr SinkDetails sink_details[] = {
                 [](std::string_view device_id) -> std::unique_ptr<Sink> {
                     return std::make_unique<NullSink>(device_id);
                 },
-                [] { return std::vector<std::string>{"null"}; }},
+                [](bool capture) { return std::vector<std::string>{"null"}; }},
 };
 
-const SinkDetails& GetSinkDetails(std::string_view sink_id) {
+const SinkDetails& GetOutputSinkDetails(std::string_view sink_id) {
     auto iter =
         std::find_if(std::begin(sink_details), std::end(sink_details),
                      [sink_id](const auto& sink_detail) { return sink_detail.id == sink_id; });
 
     if (sink_id == "auto" || iter == std::end(sink_details)) {
         if (sink_id != "auto") {
-            LOG_ERROR(Audio, "AudioCore::SelectSink given invalid sink_id {}", sink_id);
+            LOG_ERROR(Audio, "AudioCore::Sink::GetOutputSinkDetails given invalid sink_id {}",
+                      sink_id);
         }
         // Auto-select.
         // sink_details is ordered in terms of desirability, with the best choice at the front.
@@ -79,12 +80,12 @@ std::vector<const char*> GetSinkIDs() {
     return sink_ids;
 }
 
-std::vector<std::string> GetDeviceListForSink(std::string_view sink_id) {
-    return GetSinkDetails(sink_id).list_devices();
+std::vector<std::string> GetDeviceListForSink(std::string_view sink_id, bool capture) {
+    return GetOutputSinkDetails(sink_id).list_devices(capture);
 }
 
 std::unique_ptr<Sink> CreateSinkFromID(std::string_view sink_id, std::string_view device_id) {
-    return GetSinkDetails(sink_id).factory(device_id);
+    return GetOutputSinkDetails(sink_id).factory(device_id);
 }
 
-} // namespace AudioCore
+} // namespace AudioCore::Sink
diff --git a/src/audio_core/sink/sink_details.h b/src/audio_core/sink/sink_details.h
new file mode 100644
index 0000000000..3ebdb1e305
--- /dev/null
+++ b/src/audio_core/sink/sink_details.h
@@ -0,0 +1,43 @@
+// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <string>
+#include <string_view>
+#include <vector>
+
+namespace AudioCore {
+class AudioManager;
+
+namespace Sink {
+
+class Sink;
+
+/**
+ * Retrieves the IDs for all available audio sinks.
+ *
+ * @return Vector of available sink names.
+ */
+std::vector<const char*> GetSinkIDs();
+
+/**
+ * Gets the list of devices for a particular sink identified by the given ID.
+ *
+ * @param sink_id - Id of the sink to get devices from.
+ * @param capture - Get capture (input) devices, or output devices?
+ * @return Vector of device names.
+ */
+std::vector<std::string> GetDeviceListForSink(std::string_view sink_id, bool capture);
+
+/**
+ * Creates an audio sink identified by the given device ID.
+ *
+ * @param sink_id   - Id of the sink to create.
+ * @param device_id - Name of the device to create.
+ * @return Pointer to the created sink.
+ */
+std::unique_ptr<Sink> CreateSinkFromID(std::string_view sink_id, std::string_view device_id);
+
+} // namespace Sink
+} // namespace AudioCore
diff --git a/src/audio_core/sink/sink_stream.h b/src/audio_core/sink/sink_stream.h
new file mode 100644
index 0000000000..17ed6593fb
--- /dev/null
+++ b/src/audio_core/sink/sink_stream.h
@@ -0,0 +1,224 @@
+// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <atomic>
+#include <memory>
+#include <vector>
+
+#include "audio_core/common/common.h"
+#include "common/common_types.h"
+
+namespace AudioCore::Sink {
+
+enum class StreamType {
+    Render,
+    Out,
+    In,
+};
+
+struct SinkBuffer {
+    u64 frames;
+    u64 frames_played;
+    u64 tag;
+    bool consumed;
+};
+
+/**
+ * Contains a real backend stream for outputting samples to hardware,
+ * created only via a Sink (See Sink::AcquireSinkStream).
+ *
+ * Accepts a SinkBuffer and samples in PCM16 format to be output (see AppendBuffer).
+ * Appended buffers act as a FIFO queue, and will be held until played.
+ * You should regularly call IsBufferConsumed with the unique SinkBuffer tag to check if the buffer
+ * has been consumed.
+ *
+ * Since these are a FIFO queue, always check IsBufferConsumed in the same order you appended the
+ * buffers, skipping a buffer will result in all following buffers to never release.
+ *
+ * If the buffers appear to be stuck, you can stop and re-open an IAudioIn/IAudioOut service (this
+ * is what games do), or call ClearQueue to flush all of the buffers without a full restart.
+ */
+class SinkStream {
+public:
+    virtual ~SinkStream() = default;
+
+    /**
+     * Finalize the sink stream.
+     */
+    virtual void Finalize() = 0;
+
+    /**
+     * Start the sink stream.
+     *
+     * @param resume - Set to true if this is resuming the stream a previously-active stream.
+     *                 Default false.
+     */
+    virtual void Start(bool resume = false) = 0;
+
+    /**
+     * Stop the sink stream.
+     */
+    virtual void Stop() = 0;
+
+    /**
+     * Append a new buffer and its samples to a waiting queue to play.
+     *
+     * @param buffer  - Audio buffer information to be queued.
+     * @param samples - The s16 samples to be queue for playback.
+     */
+    virtual void AppendBuffer(SinkBuffer& buffer, std::vector<s16>& samples) = 0;
+
+    /**
+     * Release a buffer. Audio In only, will fill a buffer with recorded samples.
+     *
+     * @param num_samples - Maximum number of samples to receive.
+     * @return Vector of recorded samples. May have fewer than num_samples.
+     */
+    virtual std::vector<s16> ReleaseBuffer(u64 num_samples) = 0;
+
+    /**
+     * Check if a certain buffer has been consumed (fully played).
+     *
+     * @param tag - Unique tag of a buffer to check for.
+     * @return True if the buffer has been played, otherwise false.
+     */
+    virtual bool IsBufferConsumed(u64 tag) = 0;
+
+    /**
+     * Empty out the buffer queue.
+     */
+    virtual void ClearQueue() = 0;
+
+    /**
+     * Check if the stream is paused.
+     *
+     * @return True if paused, otherwise false.
+     */
+    bool IsPaused() {
+        return paused;
+    }
+
+    /**
+     * Get the number of system channels in this stream.
+     *
+     * @return Number of system channels.
+     */
+    u32 GetSystemChannels() const {
+        return system_channels;
+    }
+
+    /**
+     * Set the number of channels the system expects.
+     *
+     * @param channels - New number of system channels.
+     */
+    void SetSystemChannels(u32 channels) {
+        system_channels = channels;
+    }
+
+    /**
+     * Get the number of channels the hardware supports.
+     *
+     * @return Number of channels supported.
+     */
+    u32 GetDeviceChannels() const {
+        return device_channels;
+    }
+
+    /**
+     * Get the total number of samples played by this stream.
+     *
+     * @return Number of samples played.
+     */
+    u64 GetPlayedSampleCount() const {
+        return played_sample_count;
+    }
+
+    /**
+     * Set the number of samples played.
+     * This is started and stopped on system start/stop.
+     *
+     * @param played_sample_count_ - Number of samples to set.
+     */
+    void SetPlayedSampleCount(u64 played_sample_count_) {
+        played_sample_count = played_sample_count_;
+    }
+
+    /**
+     * Add to the played sample count.
+     *
+     * @param num_samples - Number of samples to add.
+     */
+    void AddPlayedSampleCount(u64 num_samples) {
+        played_sample_count += num_samples;
+    }
+
+    /**
+     * Get the system volume.
+     *
+     * @return The current system volume.
+     */
+    f32 GetSystemVolume() const {
+        return system_volume;
+    }
+
+    /**
+     * Get the device volume.
+     *
+     * @return The current device volume.
+     */
+    f32 GetDeviceVolume() const {
+        return device_volume;
+    }
+
+    /**
+     * Set the system volume.
+     *
+     * @param volume_ - The new system volume.
+     */
+    void SetSystemVolume(f32 volume_) {
+        system_volume = volume_;
+    }
+
+    /**
+     * Set the device volume.
+     *
+     * @param volume_ - The new device volume.
+     */
+    void SetDeviceVolume(f32 volume_) {
+        device_volume = volume_;
+    }
+
+    /**
+     * Get the number of queued audio buffers.
+     *
+     * @return The number of queued buffers.
+     */
+    u32 GetQueueSize() {
+        return queued_buffers.load();
+    }
+
+protected:
+    /// Number of buffers waiting to be played
+    std::atomic<u32> queued_buffers{};
+    /// Total samples played by this stream
+    std::atomic<u64> played_sample_count{};
+    /// Set by the audio render/in/out system which uses this stream
+    f32 system_volume{1.0f};
+    /// Set via IAudioDevice service calls
+    f32 device_volume{1.0f};
+    /// Set by the audio render/in/out systen which uses this stream
+    u32 system_channels{2};
+    /// Channels supported by hardware
+    u32 device_channels{2};
+    /// Is this stream currently paused?
+    std::atomic<bool> paused{true};
+    /// Was this stream previously playing?
+    std::atomic<bool> was_playing{false};
+};
+
+using SinkStreamPtr = std::unique_ptr<SinkStream>;
+
+} // namespace AudioCore::Sink
diff --git a/src/audio_core/sink_context.cpp b/src/audio_core/sink_context.cpp
deleted file mode 100644
index 835e12f67a..0000000000
--- a/src/audio_core/sink_context.cpp
+++ /dev/null
@@ -1,47 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include "audio_core/sink_context.h"
-
-namespace AudioCore {
-SinkContext::SinkContext(std::size_t sink_count_) : sink_count{sink_count_} {}
-SinkContext::~SinkContext() = default;
-
-std::size_t SinkContext::GetCount() const {
-    return sink_count;
-}
-
-void SinkContext::UpdateMainSink(const SinkInfo::InParams& in) {
-    ASSERT(in.type == SinkTypes::Device);
-
-    if (in.device.down_matrix_enabled) {
-        downmix_coefficients = in.device.down_matrix_coef;
-    } else {
-        downmix_coefficients = {
-            1.0f,   // front
-            0.707f, // center
-            0.0f,   // lfe
-            0.707f, // back
-        };
-    }
-
-    in_use = in.in_use;
-    use_count = in.device.input_count;
-    buffers = in.device.input;
-}
-
-bool SinkContext::InUse() const {
-    return in_use;
-}
-
-std::vector<u8> SinkContext::OutputBuffers() const {
-    std::vector<u8> buffer_ret(use_count);
-    std::memcpy(buffer_ret.data(), buffers.data(), use_count);
-    return buffer_ret;
-}
-
-const DownmixCoefficients& SinkContext::GetDownmixCoefficients() const {
-    return downmix_coefficients;
-}
-
-} // namespace AudioCore
diff --git a/src/audio_core/sink_context.h b/src/audio_core/sink_context.h
deleted file mode 100644
index cc5a90d804..0000000000
--- a/src/audio_core/sink_context.h
+++ /dev/null
@@ -1,95 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include <array>
-#include <vector>
-#include "audio_core/common.h"
-#include "common/common_funcs.h"
-#include "common/common_types.h"
-#include "common/swap.h"
-
-namespace AudioCore {
-
-using DownmixCoefficients = std::array<float_le, 4>;
-
-enum class SinkTypes : u8 {
-    Invalid = 0,
-    Device = 1,
-    Circular = 2,
-};
-
-enum class SinkSampleFormat : u32_le {
-    None = 0,
-    Pcm8 = 1,
-    Pcm16 = 2,
-    Pcm24 = 3,
-    Pcm32 = 4,
-    PcmFloat = 5,
-    Adpcm = 6,
-};
-
-class SinkInfo {
-public:
-    struct CircularBufferIn {
-        u64_le address;
-        u32_le size;
-        u32_le input_count;
-        u32_le sample_count;
-        u32_le previous_position;
-        SinkSampleFormat sample_format;
-        std::array<u8, AudioCommon::MAX_CHANNEL_COUNT> input;
-        bool in_use;
-        INSERT_PADDING_BYTES_NOINIT(5);
-    };
-    static_assert(sizeof(CircularBufferIn) == 0x28,
-                  "SinkInfo::CircularBufferIn is in invalid size");
-
-    struct DeviceIn {
-        std::array<u8, 255> device_name;
-        INSERT_PADDING_BYTES_NOINIT(1);
-        s32_le input_count;
-        std::array<u8, AudioCommon::MAX_CHANNEL_COUNT> input;
-        INSERT_PADDING_BYTES_NOINIT(1);
-        bool down_matrix_enabled;
-        DownmixCoefficients down_matrix_coef;
-    };
-    static_assert(sizeof(DeviceIn) == 0x11c, "SinkInfo::DeviceIn is an invalid size");
-
-    struct InParams {
-        SinkTypes type{};
-        bool in_use{};
-        INSERT_PADDING_BYTES(2);
-        u32_le node_id{};
-        INSERT_PADDING_WORDS(6);
-        union {
-            // std::array<u8, 0x120> raw{};
-            DeviceIn device;
-            CircularBufferIn circular_buffer;
-        };
-    };
-    static_assert(sizeof(InParams) == 0x140, "SinkInfo::InParams are an invalid size!");
-};
-
-class SinkContext {
-public:
-    explicit SinkContext(std::size_t sink_count_);
-    ~SinkContext();
-
-    [[nodiscard]] std::size_t GetCount() const;
-
-    void UpdateMainSink(const SinkInfo::InParams& in);
-    [[nodiscard]] bool InUse() const;
-    [[nodiscard]] std::vector<u8> OutputBuffers() const;
-
-    [[nodiscard]] const DownmixCoefficients& GetDownmixCoefficients() const;
-
-private:
-    bool in_use{false};
-    s32 use_count{};
-    std::array<u8, AudioCommon::MAX_CHANNEL_COUNT> buffers{};
-    std::size_t sink_count{};
-    DownmixCoefficients downmix_coefficients{};
-};
-} // namespace AudioCore
diff --git a/src/audio_core/sink_details.h b/src/audio_core/sink_details.h
deleted file mode 100644
index 0427663582..0000000000
--- a/src/audio_core/sink_details.h
+++ /dev/null
@@ -1,23 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include <string>
-#include <string_view>
-#include <vector>
-
-namespace AudioCore {
-
-class Sink;
-
-/// Retrieves the IDs for all available audio sinks.
-std::vector<const char*> GetSinkIDs();
-
-/// Gets the list of devices for a particular sink identified by the given ID.
-std::vector<std::string> GetDeviceListForSink(std::string_view sink_id);
-
-/// Creates an audio sink identified by the given device ID.
-std::unique_ptr<Sink> CreateSinkFromID(std::string_view sink_id, std::string_view device_id);
-
-} // namespace AudioCore
diff --git a/src/audio_core/sink_stream.h b/src/audio_core/sink_stream.h
deleted file mode 100644
index 0449b90afa..0000000000
--- a/src/audio_core/sink_stream.h
+++ /dev/null
@@ -1,35 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include <memory>
-#include <vector>
-
-#include "common/common_types.h"
-
-namespace AudioCore {
-
-/**
- * Accepts samples in stereo signed PCM16 format to be output. Sinks *do not* handle resampling and
- * expect the correct sample rate. They are dumb outputs.
- */
-class SinkStream {
-public:
-    virtual ~SinkStream() = default;
-
-    /**
-     * Feed stereo samples to sink.
-     * @param num_channels Number of channels used.
-     * @param samples Samples in interleaved stereo PCM16 format.
-     */
-    virtual void EnqueueSamples(u32 num_channels, const std::vector<s16>& samples) = 0;
-
-    virtual std::size_t SamplesInQueue(u32 num_channels) const = 0;
-
-    virtual void Flush() = 0;
-};
-
-using SinkStreamPtr = std::unique_ptr<SinkStream>;
-
-} // namespace AudioCore
diff --git a/src/audio_core/splitter_context.cpp b/src/audio_core/splitter_context.cpp
deleted file mode 100644
index 10646dc059..0000000000
--- a/src/audio_core/splitter_context.cpp
+++ /dev/null
@@ -1,616 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include "audio_core/behavior_info.h"
-#include "audio_core/splitter_context.h"
-#include "common/alignment.h"
-#include "common/assert.h"
-#include "common/logging/log.h"
-
-namespace AudioCore {
-
-ServerSplitterDestinationData::ServerSplitterDestinationData(s32 id_) : id{id_} {}
-ServerSplitterDestinationData::~ServerSplitterDestinationData() = default;
-
-void ServerSplitterDestinationData::Update(SplitterInfo::InDestinationParams& header) {
-    // Log error as these are not actually failure states
-    if (header.magic != SplitterMagic::DataHeader) {
-        LOG_ERROR(Audio, "Splitter destination header is invalid!");
-        return;
-    }
-
-    // Incorrect splitter id
-    if (header.splitter_id != id) {
-        LOG_ERROR(Audio, "Splitter destination ids do not match!");
-        return;
-    }
-
-    mix_id = header.mix_id;
-    // Copy our mix volumes
-    std::copy(header.mix_volumes.begin(), header.mix_volumes.end(), current_mix_volumes.begin());
-    if (!in_use && header.in_use) {
-        // Update mix volumes
-        std::copy(current_mix_volumes.begin(), current_mix_volumes.end(), last_mix_volumes.begin());
-        needs_update = false;
-    }
-    in_use = header.in_use;
-}
-
-ServerSplitterDestinationData* ServerSplitterDestinationData::GetNextDestination() {
-    return next;
-}
-
-const ServerSplitterDestinationData* ServerSplitterDestinationData::GetNextDestination() const {
-    return next;
-}
-
-void ServerSplitterDestinationData::SetNextDestination(ServerSplitterDestinationData* dest) {
-    next = dest;
-}
-
-bool ServerSplitterDestinationData::ValidMixId() const {
-    return GetMixId() != AudioCommon::NO_MIX;
-}
-
-s32 ServerSplitterDestinationData::GetMixId() const {
-    return mix_id;
-}
-
-bool ServerSplitterDestinationData::IsConfigured() const {
-    return in_use && ValidMixId();
-}
-
-float ServerSplitterDestinationData::GetMixVolume(std::size_t i) const {
-    ASSERT(i < AudioCommon::MAX_MIX_BUFFERS);
-    return current_mix_volumes.at(i);
-}
-
-const std::array<float, AudioCommon::MAX_MIX_BUFFERS>&
-ServerSplitterDestinationData::CurrentMixVolumes() const {
-    return current_mix_volumes;
-}
-
-const std::array<float, AudioCommon::MAX_MIX_BUFFERS>&
-ServerSplitterDestinationData::LastMixVolumes() const {
-    return last_mix_volumes;
-}
-
-void ServerSplitterDestinationData::MarkDirty() {
-    needs_update = true;
-}
-
-void ServerSplitterDestinationData::UpdateInternalState() {
-    if (in_use && needs_update) {
-        std::copy(current_mix_volumes.begin(), current_mix_volumes.end(), last_mix_volumes.begin());
-    }
-    needs_update = false;
-}
-
-ServerSplitterInfo::ServerSplitterInfo(s32 id_) : id(id_) {}
-ServerSplitterInfo::~ServerSplitterInfo() = default;
-
-void ServerSplitterInfo::InitializeInfos() {
-    send_length = 0;
-    head = nullptr;
-    new_connection = true;
-}
-
-void ServerSplitterInfo::ClearNewConnectionFlag() {
-    new_connection = false;
-}
-
-std::size_t ServerSplitterInfo::Update(SplitterInfo::InInfoPrams& header) {
-    if (header.send_id != id) {
-        return 0;
-    }
-
-    sample_rate = header.sample_rate;
-    new_connection = true;
-    // We need to update the size here due to the splitter bug being present and providing an
-    // incorrect size. We're suppose to also update the header here but we just ignore and continue
-    return (sizeof(s32_le) * (header.length - 1)) + (sizeof(s32_le) * 3);
-}
-
-ServerSplitterDestinationData* ServerSplitterInfo::GetHead() {
-    return head;
-}
-
-const ServerSplitterDestinationData* ServerSplitterInfo::GetHead() const {
-    return head;
-}
-
-ServerSplitterDestinationData* ServerSplitterInfo::GetData(std::size_t depth) {
-    auto* current_head = head;
-    for (std::size_t i = 0; i < depth; i++) {
-        if (current_head == nullptr) {
-            return nullptr;
-        }
-        current_head = current_head->GetNextDestination();
-    }
-    return current_head;
-}
-
-const ServerSplitterDestinationData* ServerSplitterInfo::GetData(std::size_t depth) const {
-    auto* current_head = head;
-    for (std::size_t i = 0; i < depth; i++) {
-        if (current_head == nullptr) {
-            return nullptr;
-        }
-        current_head = current_head->GetNextDestination();
-    }
-    return current_head;
-}
-
-bool ServerSplitterInfo::HasNewConnection() const {
-    return new_connection;
-}
-
-s32 ServerSplitterInfo::GetLength() const {
-    return send_length;
-}
-
-void ServerSplitterInfo::SetHead(ServerSplitterDestinationData* new_head) {
-    head = new_head;
-}
-
-void ServerSplitterInfo::SetHeadDepth(s32 length) {
-    send_length = length;
-}
-
-SplitterContext::SplitterContext() = default;
-SplitterContext::~SplitterContext() = default;
-
-void SplitterContext::Initialize(BehaviorInfo& behavior_info, std::size_t _info_count,
-                                 std::size_t _data_count) {
-    if (!behavior_info.IsSplitterSupported() || _data_count == 0 || _info_count == 0) {
-        Setup(0, 0, false);
-        return;
-    }
-    // Only initialize if we're using splitters
-    Setup(_info_count, _data_count, behavior_info.IsSplitterBugFixed());
-}
-
-bool SplitterContext::Update(const std::vector<u8>& input, std::size_t& input_offset,
-                             std::size_t& bytes_read) {
-    const auto UpdateOffsets = [&](std::size_t read) {
-        input_offset += read;
-        bytes_read += read;
-    };
-
-    if (info_count == 0 || data_count == 0) {
-        bytes_read = 0;
-        return true;
-    }
-
-    if (!AudioCommon::CanConsumeBuffer(input.size(), input_offset,
-                                       sizeof(SplitterInfo::InHeader))) {
-        LOG_ERROR(Audio, "Buffer is an invalid size!");
-        return false;
-    }
-    SplitterInfo::InHeader header{};
-    std::memcpy(&header, input.data() + input_offset, sizeof(SplitterInfo::InHeader));
-    UpdateOffsets(sizeof(SplitterInfo::InHeader));
-
-    if (header.magic != SplitterMagic::SplitterHeader) {
-        LOG_ERROR(Audio, "Invalid header magic! Expecting {:X} but got {:X}",
-                  SplitterMagic::SplitterHeader, header.magic);
-        return false;
-    }
-
-    // Clear all connections
-    for (auto& info : infos) {
-        info.ClearNewConnectionFlag();
-    }
-
-    UpdateInfo(input, input_offset, bytes_read, header.info_count);
-    UpdateData(input, input_offset, bytes_read, header.data_count);
-    const auto aligned_bytes_read = Common::AlignUp(bytes_read, 16);
-    input_offset += aligned_bytes_read - bytes_read;
-    bytes_read = aligned_bytes_read;
-    return true;
-}
-
-bool SplitterContext::UsingSplitter() const {
-    return info_count > 0 && data_count > 0;
-}
-
-ServerSplitterInfo& SplitterContext::GetInfo(std::size_t i) {
-    ASSERT(i < info_count);
-    return infos.at(i);
-}
-
-const ServerSplitterInfo& SplitterContext::GetInfo(std::size_t i) const {
-    ASSERT(i < info_count);
-    return infos.at(i);
-}
-
-ServerSplitterDestinationData& SplitterContext::GetData(std::size_t i) {
-    ASSERT(i < data_count);
-    return datas.at(i);
-}
-
-const ServerSplitterDestinationData& SplitterContext::GetData(std::size_t i) const {
-    ASSERT(i < data_count);
-    return datas.at(i);
-}
-
-ServerSplitterDestinationData* SplitterContext::GetDestinationData(std::size_t info,
-                                                                   std::size_t data) {
-    ASSERT(info < info_count);
-    auto& cur_info = GetInfo(info);
-    return cur_info.GetData(data);
-}
-
-const ServerSplitterDestinationData* SplitterContext::GetDestinationData(std::size_t info,
-                                                                         std::size_t data) const {
-    ASSERT(info < info_count);
-    const auto& cur_info = GetInfo(info);
-    return cur_info.GetData(data);
-}
-
-void SplitterContext::UpdateInternalState() {
-    if (data_count == 0) {
-        return;
-    }
-
-    for (auto& data : datas) {
-        data.UpdateInternalState();
-    }
-}
-
-std::size_t SplitterContext::GetInfoCount() const {
-    return info_count;
-}
-
-std::size_t SplitterContext::GetDataCount() const {
-    return data_count;
-}
-
-void SplitterContext::Setup(std::size_t info_count_, std::size_t data_count_,
-                            bool is_splitter_bug_fixed) {
-
-    info_count = info_count_;
-    data_count = data_count_;
-
-    for (std::size_t i = 0; i < info_count; i++) {
-        auto& splitter = infos.emplace_back(static_cast<s32>(i));
-        splitter.InitializeInfos();
-    }
-    for (std::size_t i = 0; i < data_count; i++) {
-        datas.emplace_back(static_cast<s32>(i));
-    }
-
-    bug_fixed = is_splitter_bug_fixed;
-}
-
-bool SplitterContext::UpdateInfo(const std::vector<u8>& input, std::size_t& input_offset,
-                                 std::size_t& bytes_read, s32 in_splitter_count) {
-    const auto UpdateOffsets = [&](std::size_t read) {
-        input_offset += read;
-        bytes_read += read;
-    };
-
-    for (s32 i = 0; i < in_splitter_count; i++) {
-        if (!AudioCommon::CanConsumeBuffer(input.size(), input_offset,
-                                           sizeof(SplitterInfo::InInfoPrams))) {
-            LOG_ERROR(Audio, "Buffer is an invalid size!");
-            return false;
-        }
-        SplitterInfo::InInfoPrams header{};
-        std::memcpy(&header, input.data() + input_offset, sizeof(SplitterInfo::InInfoPrams));
-
-        // Logged as warning as these don't actually cause a bailout for some reason
-        if (header.magic != SplitterMagic::InfoHeader) {
-            LOG_ERROR(Audio, "Bad splitter data header");
-            break;
-        }
-
-        if (header.send_id < 0 || static_cast<std::size_t>(header.send_id) > info_count) {
-            LOG_ERROR(Audio, "Bad splitter data id");
-            break;
-        }
-
-        UpdateOffsets(sizeof(SplitterInfo::InInfoPrams));
-        auto& info = GetInfo(header.send_id);
-        if (!RecomposeDestination(info, header, input, input_offset)) {
-            LOG_ERROR(Audio, "Failed to recompose destination for splitter!");
-            return false;
-        }
-        const std::size_t read = info.Update(header);
-        bytes_read += read;
-        input_offset += read;
-    }
-    return true;
-}
-
-bool SplitterContext::UpdateData(const std::vector<u8>& input, std::size_t& input_offset,
-                                 std::size_t& bytes_read, s32 in_data_count) {
-    const auto UpdateOffsets = [&](std::size_t read) {
-        input_offset += read;
-        bytes_read += read;
-    };
-
-    for (s32 i = 0; i < in_data_count; i++) {
-        if (!AudioCommon::CanConsumeBuffer(input.size(), input_offset,
-                                           sizeof(SplitterInfo::InDestinationParams))) {
-            LOG_ERROR(Audio, "Buffer is an invalid size!");
-            return false;
-        }
-        SplitterInfo::InDestinationParams header{};
-        std::memcpy(&header, input.data() + input_offset,
-                    sizeof(SplitterInfo::InDestinationParams));
-        UpdateOffsets(sizeof(SplitterInfo::InDestinationParams));
-
-        // Logged as warning as these don't actually cause a bailout for some reason
-        if (header.magic != SplitterMagic::DataHeader) {
-            LOG_ERROR(Audio, "Bad splitter data header");
-            break;
-        }
-
-        if (header.splitter_id < 0 || static_cast<std::size_t>(header.splitter_id) > data_count) {
-            LOG_ERROR(Audio, "Bad splitter data id");
-            break;
-        }
-        GetData(header.splitter_id).Update(header);
-    }
-    return true;
-}
-
-bool SplitterContext::RecomposeDestination(ServerSplitterInfo& info,
-                                           SplitterInfo::InInfoPrams& header,
-                                           const std::vector<u8>& input,
-                                           const std::size_t& input_offset) {
-    // Clear our current destinations
-    auto* current_head = info.GetHead();
-    while (current_head != nullptr) {
-        auto* next_head = current_head->GetNextDestination();
-        current_head->SetNextDestination(nullptr);
-        current_head = next_head;
-    }
-    info.SetHead(nullptr);
-
-    s32 size = header.length;
-    // If the splitter bug is present, calculate fixed size
-    if (!bug_fixed) {
-        if (info_count > 0) {
-            const auto factor = data_count / info_count;
-            size = std::min(header.length, static_cast<s32>(factor));
-        } else {
-            size = 0;
-        }
-    }
-
-    if (size < 1) {
-        LOG_ERROR(Audio, "Invalid splitter info size! size={:X}", size);
-        return true;
-    }
-
-    auto* start_head = &GetData(header.resource_id_base);
-    current_head = start_head;
-    std::vector<s32_le> resource_ids(size - 1);
-    if (!AudioCommon::CanConsumeBuffer(input.size(), input_offset,
-                                       resource_ids.size() * sizeof(s32_le))) {
-        LOG_ERROR(Audio, "Buffer is an invalid size!");
-        return false;
-    }
-    std::memcpy(resource_ids.data(), input.data() + input_offset,
-                resource_ids.size() * sizeof(s32_le));
-
-    for (auto resource_id : resource_ids) {
-        auto* head = &GetData(resource_id);
-        current_head->SetNextDestination(head);
-        current_head = head;
-    }
-
-    info.SetHead(start_head);
-    info.SetHeadDepth(size);
-
-    return true;
-}
-
-NodeStates::NodeStates() = default;
-NodeStates::~NodeStates() = default;
-
-void NodeStates::Initialize(std::size_t node_count_) {
-    // Setup our work parameters
-    node_count = node_count_;
-    was_node_found.resize(node_count);
-    was_node_completed.resize(node_count);
-    index_list.resize(node_count);
-    index_stack.Reset(node_count * node_count);
-}
-
-bool NodeStates::Tsort(EdgeMatrix& edge_matrix) {
-    return DepthFirstSearch(edge_matrix);
-}
-
-std::size_t NodeStates::GetIndexPos() const {
-    return index_pos;
-}
-
-const std::vector<s32>& NodeStates::GetIndexList() const {
-    return index_list;
-}
-
-void NodeStates::PushTsortResult(s32 index) {
-    ASSERT(index < static_cast<s32>(node_count));
-    index_list[index_pos++] = index;
-}
-
-bool NodeStates::DepthFirstSearch(EdgeMatrix& edge_matrix) {
-    ResetState();
-    for (std::size_t i = 0; i < node_count; i++) {
-        const auto node_id = static_cast<s32>(i);
-
-        // If we don't have a state, send to our index stack for work
-        if (GetState(i) == NodeStates::State::NoState) {
-            index_stack.push(node_id);
-        }
-
-        // While we have work to do in our stack
-        while (index_stack.Count() > 0) {
-            // Get the current node
-            const auto current_stack_index = index_stack.top();
-            // Check if we've seen the node yet
-            const auto index_state = GetState(current_stack_index);
-            if (index_state == NodeStates::State::NoState) {
-                // Mark the node as seen
-                UpdateState(NodeStates::State::InFound, current_stack_index);
-            } else if (index_state == NodeStates::State::InFound) {
-                // We've seen this node before, mark it as completed
-                UpdateState(NodeStates::State::InCompleted, current_stack_index);
-                // Update our index list
-                PushTsortResult(current_stack_index);
-                // Pop the stack
-                index_stack.pop();
-                continue;
-            } else if (index_state == NodeStates::State::InCompleted) {
-                // If our node is already sorted, clear it
-                index_stack.pop();
-                continue;
-            }
-
-            const auto edge_node_count = edge_matrix.GetNodeCount();
-            for (s32 j = 0; j < static_cast<s32>(edge_node_count); j++) {
-                // Check if our node is connected to our edge matrix
-                if (!edge_matrix.Connected(current_stack_index, j)) {
-                    continue;
-                }
-
-                // Check if our node exists
-                const auto node_state = GetState(j);
-                if (node_state == NodeStates::State::NoState) {
-                    // Add more work
-                    index_stack.push(j);
-                } else if (node_state == NodeStates::State::InFound) {
-                    ASSERT_MSG(false, "Node start marked as found");
-                    ResetState();
-                    return false;
-                }
-            }
-        }
-    }
-    return true;
-}
-
-void NodeStates::ResetState() {
-    // Reset to the start of our index stack
-    index_pos = 0;
-    for (std::size_t i = 0; i < node_count; i++) {
-        // Mark all nodes as not found
-        was_node_found[i] = false;
-        // Mark all nodes as uncompleted
-        was_node_completed[i] = false;
-        // Mark all indexes as invalid
-        index_list[i] = -1;
-    }
-}
-
-void NodeStates::UpdateState(NodeStates::State state, std::size_t i) {
-    switch (state) {
-    case NodeStates::State::NoState:
-        was_node_found[i] = false;
-        was_node_completed[i] = false;
-        break;
-    case NodeStates::State::InFound:
-        was_node_found[i] = true;
-        was_node_completed[i] = false;
-        break;
-    case NodeStates::State::InCompleted:
-        was_node_found[i] = false;
-        was_node_completed[i] = true;
-        break;
-    }
-}
-
-NodeStates::State NodeStates::GetState(std::size_t i) {
-    ASSERT(i < node_count);
-    if (was_node_found[i]) {
-        // If our node exists in our found list
-        return NodeStates::State::InFound;
-    } else if (was_node_completed[i]) {
-        // If node is in the completed list
-        return NodeStates::State::InCompleted;
-    } else {
-        // If in neither
-        return NodeStates::State::NoState;
-    }
-}
-
-NodeStates::Stack::Stack() = default;
-NodeStates::Stack::~Stack() = default;
-
-void NodeStates::Stack::Reset(std::size_t size) {
-    // Mark our stack as empty
-    stack.resize(size);
-    stack_size = size;
-    stack_pos = 0;
-    std::fill(stack.begin(), stack.end(), 0);
-}
-
-void NodeStates::Stack::push(s32 val) {
-    ASSERT(stack_pos < stack_size);
-    stack[stack_pos++] = val;
-}
-
-std::size_t NodeStates::Stack::Count() const {
-    return stack_pos;
-}
-
-s32 NodeStates::Stack::top() const {
-    ASSERT(stack_pos > 0);
-    return stack[stack_pos - 1];
-}
-
-s32 NodeStates::Stack::pop() {
-    ASSERT(stack_pos > 0);
-    stack_pos--;
-    return stack[stack_pos];
-}
-
-EdgeMatrix::EdgeMatrix() = default;
-EdgeMatrix::~EdgeMatrix() = default;
-
-void EdgeMatrix::Initialize(std::size_t _node_count) {
-    node_count = _node_count;
-    edge_matrix.resize(node_count * node_count);
-}
-
-bool EdgeMatrix::Connected(s32 a, s32 b) {
-    return GetState(a, b);
-}
-
-void EdgeMatrix::Connect(s32 a, s32 b) {
-    SetState(a, b, true);
-}
-
-void EdgeMatrix::Disconnect(s32 a, s32 b) {
-    SetState(a, b, false);
-}
-
-void EdgeMatrix::RemoveEdges(s32 edge) {
-    for (std::size_t i = 0; i < node_count; i++) {
-        SetState(edge, static_cast<s32>(i), false);
-    }
-}
-
-std::size_t EdgeMatrix::GetNodeCount() const {
-    return node_count;
-}
-
-void EdgeMatrix::SetState(s32 a, s32 b, bool state) {
-    ASSERT(InRange(a, b));
-    edge_matrix.at(a * node_count + b) = state;
-}
-
-bool EdgeMatrix::GetState(s32 a, s32 b) {
-    ASSERT(InRange(a, b));
-    return edge_matrix.at(a * node_count + b);
-}
-
-bool EdgeMatrix::InRange(s32 a, s32 b) const {
-    const std::size_t pos = a * node_count + b;
-    return pos < (node_count * node_count);
-}
-
-} // namespace AudioCore
diff --git a/src/audio_core/splitter_context.h b/src/audio_core/splitter_context.h
deleted file mode 100644
index 3a4b055eb5..0000000000
--- a/src/audio_core/splitter_context.h
+++ /dev/null
@@ -1,218 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include <stack>
-#include <vector>
-#include "audio_core/common.h"
-#include "common/common_funcs.h"
-#include "common/common_types.h"
-#include "common/swap.h"
-
-namespace AudioCore {
-class BehaviorInfo;
-
-class EdgeMatrix {
-public:
-    EdgeMatrix();
-    ~EdgeMatrix();
-
-    void Initialize(std::size_t _node_count);
-    bool Connected(s32 a, s32 b);
-    void Connect(s32 a, s32 b);
-    void Disconnect(s32 a, s32 b);
-    void RemoveEdges(s32 edge);
-    std::size_t GetNodeCount() const;
-
-private:
-    void SetState(s32 a, s32 b, bool state);
-    bool GetState(s32 a, s32 b);
-
-    bool InRange(s32 a, s32 b) const;
-    std::vector<bool> edge_matrix{};
-    std::size_t node_count{};
-};
-
-class NodeStates {
-public:
-    enum class State {
-        NoState = 0,
-        InFound = 1,
-        InCompleted = 2,
-    };
-
-    // Looks to be a fixed size stack. Placed within the NodeStates class based on symbols
-    class Stack {
-    public:
-        Stack();
-        ~Stack();
-
-        void Reset(std::size_t size);
-        void push(s32 val);
-        std::size_t Count() const;
-        s32 top() const;
-        s32 pop();
-
-    private:
-        std::vector<s32> stack{};
-        std::size_t stack_size{};
-        std::size_t stack_pos{};
-    };
-    NodeStates();
-    ~NodeStates();
-
-    void Initialize(std::size_t node_count_);
-    bool Tsort(EdgeMatrix& edge_matrix);
-    std::size_t GetIndexPos() const;
-    const std::vector<s32>& GetIndexList() const;
-
-private:
-    void PushTsortResult(s32 index);
-    bool DepthFirstSearch(EdgeMatrix& edge_matrix);
-    void ResetState();
-    void UpdateState(State state, std::size_t i);
-    State GetState(std::size_t i);
-
-    std::size_t node_count{};
-    std::vector<bool> was_node_found{};
-    std::vector<bool> was_node_completed{};
-    std::size_t index_pos{};
-    std::vector<s32> index_list{};
-    Stack index_stack{};
-};
-
-enum class SplitterMagic : u32_le {
-    SplitterHeader = Common::MakeMagic('S', 'N', 'D', 'H'),
-    DataHeader = Common::MakeMagic('S', 'N', 'D', 'D'),
-    InfoHeader = Common::MakeMagic('S', 'N', 'D', 'I'),
-};
-
-class SplitterInfo {
-public:
-    struct InHeader {
-        SplitterMagic magic{};
-        s32_le info_count{};
-        s32_le data_count{};
-        INSERT_PADDING_WORDS(5);
-    };
-    static_assert(sizeof(InHeader) == 0x20, "SplitterInfo::InHeader is an invalid size");
-
-    struct InInfoPrams {
-        SplitterMagic magic{};
-        s32_le send_id{};
-        s32_le sample_rate{};
-        s32_le length{};
-        s32_le resource_id_base{};
-    };
-    static_assert(sizeof(InInfoPrams) == 0x14, "SplitterInfo::InInfoPrams is an invalid size");
-
-    struct InDestinationParams {
-        SplitterMagic magic{};
-        s32_le splitter_id{};
-        std::array<float_le, AudioCommon::MAX_MIX_BUFFERS> mix_volumes{};
-        s32_le mix_id{};
-        bool in_use{};
-        INSERT_PADDING_BYTES(3);
-    };
-    static_assert(sizeof(InDestinationParams) == 0x70,
-                  "SplitterInfo::InDestinationParams is an invalid size");
-};
-
-class ServerSplitterDestinationData {
-public:
-    explicit ServerSplitterDestinationData(s32 id_);
-    ~ServerSplitterDestinationData();
-
-    void Update(SplitterInfo::InDestinationParams& header);
-
-    ServerSplitterDestinationData* GetNextDestination();
-    const ServerSplitterDestinationData* GetNextDestination() const;
-    void SetNextDestination(ServerSplitterDestinationData* dest);
-    bool ValidMixId() const;
-    s32 GetMixId() const;
-    bool IsConfigured() const;
-    float GetMixVolume(std::size_t i) const;
-    const std::array<float, AudioCommon::MAX_MIX_BUFFERS>& CurrentMixVolumes() const;
-    const std::array<float, AudioCommon::MAX_MIX_BUFFERS>& LastMixVolumes() const;
-    void MarkDirty();
-    void UpdateInternalState();
-
-private:
-    bool needs_update{};
-    bool in_use{};
-    s32 id{};
-    s32 mix_id{};
-    std::array<float, AudioCommon::MAX_MIX_BUFFERS> current_mix_volumes{};
-    std::array<float, AudioCommon::MAX_MIX_BUFFERS> last_mix_volumes{};
-    ServerSplitterDestinationData* next = nullptr;
-};
-
-class ServerSplitterInfo {
-public:
-    explicit ServerSplitterInfo(s32 id_);
-    ~ServerSplitterInfo();
-
-    void InitializeInfos();
-    void ClearNewConnectionFlag();
-    std::size_t Update(SplitterInfo::InInfoPrams& header);
-
-    ServerSplitterDestinationData* GetHead();
-    const ServerSplitterDestinationData* GetHead() const;
-    ServerSplitterDestinationData* GetData(std::size_t depth);
-    const ServerSplitterDestinationData* GetData(std::size_t depth) const;
-
-    bool HasNewConnection() const;
-    s32 GetLength() const;
-
-    void SetHead(ServerSplitterDestinationData* new_head);
-    void SetHeadDepth(s32 length);
-
-private:
-    s32 sample_rate{};
-    s32 id{};
-    s32 send_length{};
-    ServerSplitterDestinationData* head = nullptr;
-    bool new_connection{};
-};
-
-class SplitterContext {
-public:
-    SplitterContext();
-    ~SplitterContext();
-
-    void Initialize(BehaviorInfo& behavior_info, std::size_t splitter_count,
-                    std::size_t data_count);
-
-    bool Update(const std::vector<u8>& input, std::size_t& input_offset, std::size_t& bytes_read);
-    bool UsingSplitter() const;
-
-    ServerSplitterInfo& GetInfo(std::size_t i);
-    const ServerSplitterInfo& GetInfo(std::size_t i) const;
-    ServerSplitterDestinationData& GetData(std::size_t i);
-    const ServerSplitterDestinationData& GetData(std::size_t i) const;
-    ServerSplitterDestinationData* GetDestinationData(std::size_t info, std::size_t data);
-    const ServerSplitterDestinationData* GetDestinationData(std::size_t info,
-                                                            std::size_t data) const;
-    void UpdateInternalState();
-
-    std::size_t GetInfoCount() const;
-    std::size_t GetDataCount() const;
-
-private:
-    void Setup(std::size_t info_count, std::size_t data_count, bool is_splitter_bug_fixed);
-    bool UpdateInfo(const std::vector<u8>& input, std::size_t& input_offset,
-                    std::size_t& bytes_read, s32 in_splitter_count);
-    bool UpdateData(const std::vector<u8>& input, std::size_t& input_offset,
-                    std::size_t& bytes_read, s32 in_data_count);
-    bool RecomposeDestination(ServerSplitterInfo& info, SplitterInfo::InInfoPrams& header,
-                              const std::vector<u8>& input, const std::size_t& input_offset);
-
-    std::vector<ServerSplitterInfo> infos{};
-    std::vector<ServerSplitterDestinationData> datas{};
-
-    std::size_t info_count{};
-    std::size_t data_count{};
-    bool bug_fixed{};
-};
-} // namespace AudioCore
diff --git a/src/audio_core/stream.cpp b/src/audio_core/stream.cpp
deleted file mode 100644
index cf3d94c537..0000000000
--- a/src/audio_core/stream.cpp
+++ /dev/null
@@ -1,175 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include <algorithm>
-#include <cmath>
-
-#include "audio_core/sink.h"
-#include "audio_core/sink_details.h"
-#include "audio_core/sink_stream.h"
-#include "audio_core/stream.h"
-#include "common/assert.h"
-#include "common/logging/log.h"
-#include "common/settings.h"
-#include "core/core_timing.h"
-
-namespace AudioCore {
-
-constexpr std::size_t MaxAudioBufferCount{32};
-
-u32 Stream::GetNumChannels() const {
-    switch (format) {
-    case Format::Mono16:
-        return 1;
-    case Format::Stereo16:
-        return 2;
-    case Format::Multi51Channel16:
-        return 6;
-    }
-    UNIMPLEMENTED_MSG("Unimplemented format={}", static_cast<u32>(format));
-    return {};
-}
-
-Stream::Stream(Core::Timing::CoreTiming& core_timing_, u32 sample_rate_, Format format_,
-               ReleaseCallback&& release_callback_, SinkStream& sink_stream_, std::string&& name_)
-    : sample_rate{sample_rate_}, format{format_}, release_callback{std::move(release_callback_)},
-      sink_stream{sink_stream_}, core_timing{core_timing_}, name{std::move(name_)} {
-    release_event = Core::Timing::CreateEvent(
-        name, [this](std::uintptr_t, s64 time, std::chrono::nanoseconds ns_late) {
-            ReleaseActiveBuffer(ns_late);
-            return std::nullopt;
-        });
-}
-
-void Stream::Play() {
-    state = State::Playing;
-    PlayNextBuffer();
-}
-
-void Stream::Stop() {
-    state = State::Stopped;
-    UNIMPLEMENTED();
-}
-
-bool Stream::Flush() {
-    const bool had_buffers = !queued_buffers.empty();
-    while (!queued_buffers.empty()) {
-        queued_buffers.pop();
-    }
-    return had_buffers;
-}
-
-void Stream::SetVolume(float volume) {
-    game_volume = volume;
-}
-
-Stream::State Stream::GetState() const {
-    return state;
-}
-
-std::chrono::nanoseconds Stream::GetBufferReleaseNS(const Buffer& buffer) const {
-    const std::size_t num_samples{buffer.GetSamples().size() / GetNumChannels()};
-    return std::chrono::nanoseconds((static_cast<u64>(num_samples) * 1000000000ULL) / sample_rate);
-}
-
-static void VolumeAdjustSamples(std::vector<s16>& samples, float game_volume) {
-    const float volume{std::clamp(Settings::Volume() - (1.0f - game_volume), 0.0f, 1.0f)};
-
-    if (volume == 1.0f) {
-        return;
-    }
-
-    // Perceived volume is not the same as the volume level
-    const float volume_scale_factor = (0.85f * ((volume * volume) - volume)) + volume;
-    for (auto& sample : samples) {
-        sample = static_cast<s16>(sample * volume_scale_factor);
-    }
-}
-
-void Stream::PlayNextBuffer(std::chrono::nanoseconds ns_late) {
-    if (!IsPlaying()) {
-        // Ensure we are in playing state before playing the next buffer
-        sink_stream.Flush();
-        return;
-    }
-
-    if (active_buffer) {
-        // Do not queue a new buffer if we are already playing a buffer
-        return;
-    }
-
-    if (queued_buffers.empty()) {
-        // No queued buffers - we are effectively paused
-        sink_stream.Flush();
-        return;
-    }
-
-    active_buffer = queued_buffers.front();
-    queued_buffers.pop();
-
-    auto& samples = active_buffer->GetSamples();
-
-    VolumeAdjustSamples(samples, game_volume);
-
-    sink_stream.EnqueueSamples(GetNumChannels(), samples);
-    played_samples += samples.size();
-
-    const auto buffer_release_ns = GetBufferReleaseNS(*active_buffer);
-
-    // If ns_late is higher than the update rate ignore the delay
-    if (ns_late > buffer_release_ns) {
-        ns_late = {};
-    }
-
-    core_timing.ScheduleEvent(buffer_release_ns - ns_late, release_event, {});
-}
-
-void Stream::ReleaseActiveBuffer(std::chrono::nanoseconds ns_late) {
-    ASSERT(active_buffer);
-    released_buffers.push(std::move(active_buffer));
-    release_callback();
-    PlayNextBuffer(ns_late);
-}
-
-bool Stream::QueueBuffer(BufferPtr&& buffer) {
-    if (queued_buffers.size() < MaxAudioBufferCount) {
-        queued_buffers.push(std::move(buffer));
-        PlayNextBuffer();
-        return true;
-    }
-    return false;
-}
-
-bool Stream::ContainsBuffer([[maybe_unused]] Buffer::Tag tag) const {
-    UNIMPLEMENTED();
-    return {};
-}
-
-std::vector<Buffer::Tag> Stream::GetTagsAndReleaseBuffers(std::size_t max_count) {
-    std::vector<Buffer::Tag> tags;
-    for (std::size_t count = 0; count < max_count && !released_buffers.empty(); ++count) {
-        if (released_buffers.front()) {
-            tags.push_back(released_buffers.front()->GetTag());
-        } else {
-            ASSERT_MSG(false, "Invalid tag in released_buffers!");
-        }
-        released_buffers.pop();
-    }
-    return tags;
-}
-
-std::vector<Buffer::Tag> Stream::GetTagsAndReleaseBuffers() {
-    std::vector<Buffer::Tag> tags;
-    tags.reserve(released_buffers.size());
-    while (!released_buffers.empty()) {
-        if (released_buffers.front()) {
-            tags.push_back(released_buffers.front()->GetTag());
-        } else {
-            ASSERT_MSG(false, "Invalid tag in released_buffers!");
-        }
-        released_buffers.pop();
-    }
-    return tags;
-}
-
-} // namespace AudioCore
diff --git a/src/audio_core/stream.h b/src/audio_core/stream.h
deleted file mode 100644
index f5de703962..0000000000
--- a/src/audio_core/stream.h
+++ /dev/null
@@ -1,130 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include <chrono>
-#include <functional>
-#include <memory>
-#include <string>
-#include <vector>
-#include <queue>
-
-#include "audio_core/buffer.h"
-#include "common/common_types.h"
-
-namespace Core::Timing {
-class CoreTiming;
-struct EventType;
-} // namespace Core::Timing
-
-namespace AudioCore {
-
-class SinkStream;
-
-/**
- * Represents an audio stream, which is a sequence of queued buffers, to be outputed by AudioOut
- */
-class Stream {
-public:
-    /// Audio format of the stream
-    enum class Format {
-        Mono16,
-        Stereo16,
-        Multi51Channel16,
-    };
-
-    /// Current state of the stream
-    enum class State {
-        Stopped,
-        Playing,
-    };
-
-    /// Callback function type, used to change guest state on a buffer being released
-    using ReleaseCallback = std::function<void()>;
-
-    Stream(Core::Timing::CoreTiming& core_timing_, u32 sample_rate_, Format format_,
-           ReleaseCallback&& release_callback_, SinkStream& sink_stream_, std::string&& name_);
-
-    /// Plays the audio stream
-    void Play();
-
-    /// Stops the audio stream
-    void Stop();
-
-    /// Queues a buffer into the audio stream, returns true on success
-    bool QueueBuffer(BufferPtr&& buffer);
-
-    /// Flush audio buffers
-    bool Flush();
-
-    /// Returns true if the audio stream contains a buffer with the specified tag
-    [[nodiscard]] bool ContainsBuffer(Buffer::Tag tag) const;
-
-    /// Returns a vector of recently released buffers specified by tag
-    [[nodiscard]] std::vector<Buffer::Tag> GetTagsAndReleaseBuffers(std::size_t max_count);
-
-    /// Returns a vector of all recently released buffers specified by tag
-    [[nodiscard]] std::vector<Buffer::Tag> GetTagsAndReleaseBuffers();
-
-    void SetVolume(float volume);
-
-    [[nodiscard]] float GetVolume() const {
-        return game_volume;
-    }
-
-    /// Returns true if the stream is currently playing
-    [[nodiscard]] bool IsPlaying() const {
-        return state == State::Playing;
-    }
-
-    /// Returns the number of queued buffers
-    [[nodiscard]] std::size_t GetQueueSize() const {
-        return queued_buffers.size();
-    }
-
-    /// Gets the sample rate
-    [[nodiscard]] u32 GetSampleRate() const {
-        return sample_rate;
-    }
-
-    /// Gets the number of samples played so far
-    [[nodiscard]] u64 GetPlayedSampleCount() const {
-        return played_samples;
-    }
-
-    /// Gets the number of channels
-    [[nodiscard]] u32 GetNumChannels() const;
-
-    /// Get the state
-    [[nodiscard]] State GetState() const;
-
-private:
-    /// Plays the next queued buffer in the audio stream, starting playback if necessary
-    void PlayNextBuffer(std::chrono::nanoseconds ns_late = {});
-
-    /// Releases the actively playing buffer, signalling that it has been completed
-    void ReleaseActiveBuffer(std::chrono::nanoseconds ns_late = {});
-
-    /// Gets the number of core cycles when the specified buffer will be released
-    [[nodiscard]] std::chrono::nanoseconds GetBufferReleaseNS(const Buffer& buffer) const;
-
-    u32 sample_rate;                  ///< Sample rate of the stream
-    u64 played_samples{};             ///< The current played sample count
-    Format format;                    ///< Format of the stream
-    float game_volume = 1.0f;         ///< The volume the game currently has set
-    ReleaseCallback release_callback; ///< Buffer release callback for the stream
-    State state{State::Stopped};      ///< Playback state of the stream
-    std::shared_ptr<Core::Timing::EventType>
-        release_event;                      ///< Core timing release event for the stream
-    BufferPtr active_buffer;                ///< Actively playing buffer in the stream
-    std::queue<BufferPtr> queued_buffers;   ///< Buffers queued to be played in the stream
-    std::queue<BufferPtr> released_buffers; ///< Buffers recently released from the stream
-    SinkStream& sink_stream;                ///< Output sink for the stream
-    Core::Timing::CoreTiming& core_timing;  ///< Core timing instance.
-    std::string name;                       ///< Name of the stream, must be unique
-};
-
-using StreamPtr = std::shared_ptr<Stream>;
-
-} // namespace AudioCore
diff --git a/src/audio_core/voice_context.cpp b/src/audio_core/voice_context.cpp
deleted file mode 100644
index f58a5c7545..0000000000
--- a/src/audio_core/voice_context.cpp
+++ /dev/null
@@ -1,579 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include <algorithm>
-
-#include "audio_core/behavior_info.h"
-#include "audio_core/voice_context.h"
-#include "core/memory.h"
-
-namespace AudioCore {
-
-ServerVoiceChannelResource::ServerVoiceChannelResource(s32 id_) : id(id_) {}
-ServerVoiceChannelResource::~ServerVoiceChannelResource() = default;
-
-bool ServerVoiceChannelResource::InUse() const {
-    return in_use;
-}
-
-float ServerVoiceChannelResource::GetCurrentMixVolumeAt(std::size_t i) const {
-    ASSERT(i < AudioCommon::MAX_MIX_BUFFERS);
-    return mix_volume.at(i);
-}
-
-float ServerVoiceChannelResource::GetLastMixVolumeAt(std::size_t i) const {
-    ASSERT(i < AudioCommon::MAX_MIX_BUFFERS);
-    return last_mix_volume.at(i);
-}
-
-void ServerVoiceChannelResource::Update(VoiceChannelResource::InParams& in_params) {
-    in_use = in_params.in_use;
-    // Update our mix volumes only if it's in use
-    if (in_params.in_use) {
-        mix_volume = in_params.mix_volume;
-    }
-}
-
-void ServerVoiceChannelResource::UpdateLastMixVolumes() {
-    last_mix_volume = mix_volume;
-}
-
-const std::array<float, AudioCommon::MAX_MIX_BUFFERS>&
-ServerVoiceChannelResource::GetCurrentMixVolume() const {
-    return mix_volume;
-}
-
-const std::array<float, AudioCommon::MAX_MIX_BUFFERS>&
-ServerVoiceChannelResource::GetLastMixVolume() const {
-    return last_mix_volume;
-}
-
-ServerVoiceInfo::ServerVoiceInfo() {
-    Initialize();
-}
-ServerVoiceInfo::~ServerVoiceInfo() = default;
-
-void ServerVoiceInfo::Initialize() {
-    in_params.in_use = false;
-    in_params.node_id = 0;
-    in_params.id = 0;
-    in_params.current_playstate = ServerPlayState::Stop;
-    in_params.priority = 255;
-    in_params.sample_rate = 0;
-    in_params.sample_format = SampleFormat::Invalid;
-    in_params.channel_count = 0;
-    in_params.pitch = 0.0f;
-    in_params.volume = 0.0f;
-    in_params.last_volume = 0.0f;
-    in_params.biquad_filter.fill({});
-    in_params.wave_buffer_count = 0;
-    in_params.wave_buffer_head = 0;
-    in_params.mix_id = AudioCommon::NO_MIX;
-    in_params.splitter_info_id = AudioCommon::NO_SPLITTER;
-    in_params.additional_params_address = 0;
-    in_params.additional_params_size = 0;
-    in_params.is_new = false;
-    out_params.played_sample_count = 0;
-    out_params.wave_buffer_consumed = 0;
-    in_params.voice_drop_flag = false;
-    in_params.buffer_mapped = true;
-    in_params.wave_buffer_flush_request_count = 0;
-    in_params.was_biquad_filter_enabled.fill(false);
-
-    for (auto& wave_buffer : in_params.wave_buffer) {
-        wave_buffer.start_sample_offset = 0;
-        wave_buffer.end_sample_offset = 0;
-        wave_buffer.is_looping = false;
-        wave_buffer.end_of_stream = false;
-        wave_buffer.buffer_address = 0;
-        wave_buffer.buffer_size = 0;
-        wave_buffer.context_address = 0;
-        wave_buffer.context_size = 0;
-        wave_buffer.sent_to_dsp = true;
-    }
-
-    stored_samples.clear();
-}
-
-void ServerVoiceInfo::UpdateParameters(const VoiceInfo::InParams& voice_in,
-                                       BehaviorInfo& behavior_info) {
-    in_params.in_use = voice_in.is_in_use;
-    in_params.id = voice_in.id;
-    in_params.node_id = voice_in.node_id;
-    in_params.last_playstate = in_params.current_playstate;
-    switch (voice_in.play_state) {
-    case PlayState::Paused:
-        in_params.current_playstate = ServerPlayState::Paused;
-        break;
-    case PlayState::Stopped:
-        if (in_params.current_playstate != ServerPlayState::Stop) {
-            in_params.current_playstate = ServerPlayState::RequestStop;
-        }
-        break;
-    case PlayState::Started:
-        in_params.current_playstate = ServerPlayState::Play;
-        break;
-    default:
-        ASSERT_MSG(false, "Unknown playstate {}", voice_in.play_state);
-        break;
-    }
-
-    in_params.priority = voice_in.priority;
-    in_params.sorting_order = voice_in.sorting_order;
-    in_params.sample_rate = voice_in.sample_rate;
-    in_params.sample_format = voice_in.sample_format;
-    in_params.channel_count = voice_in.channel_count;
-    in_params.pitch = voice_in.pitch;
-    in_params.volume = voice_in.volume;
-    in_params.biquad_filter = voice_in.biquad_filter;
-    in_params.wave_buffer_count = voice_in.wave_buffer_count;
-    in_params.wave_buffer_head = voice_in.wave_buffer_head;
-    if (behavior_info.IsFlushVoiceWaveBuffersSupported()) {
-        const auto in_request_count = in_params.wave_buffer_flush_request_count;
-        const auto voice_request_count = voice_in.wave_buffer_flush_request_count;
-        in_params.wave_buffer_flush_request_count =
-            static_cast<u8>(in_request_count + voice_request_count);
-    }
-    in_params.mix_id = voice_in.mix_id;
-    if (behavior_info.IsSplitterSupported()) {
-        in_params.splitter_info_id = voice_in.splitter_info_id;
-    } else {
-        in_params.splitter_info_id = AudioCommon::NO_SPLITTER;
-    }
-
-    std::memcpy(in_params.voice_channel_resource_id.data(),
-                voice_in.voice_channel_resource_ids.data(),
-                sizeof(s32) * in_params.voice_channel_resource_id.size());
-
-    if (behavior_info.IsVoicePlayedSampleCountResetAtLoopPointSupported()) {
-        in_params.behavior_flags.is_played_samples_reset_at_loop_point =
-            voice_in.behavior_flags.is_played_samples_reset_at_loop_point;
-    } else {
-        in_params.behavior_flags.is_played_samples_reset_at_loop_point.Assign(0);
-    }
-    if (behavior_info.IsVoicePitchAndSrcSkippedSupported()) {
-        in_params.behavior_flags.is_pitch_and_src_skipped =
-            voice_in.behavior_flags.is_pitch_and_src_skipped;
-    } else {
-        in_params.behavior_flags.is_pitch_and_src_skipped.Assign(0);
-    }
-
-    if (voice_in.is_voice_drop_flag_clear_requested) {
-        in_params.voice_drop_flag = false;
-    }
-
-    if (in_params.additional_params_address != voice_in.additional_params_address ||
-        in_params.additional_params_size != voice_in.additional_params_size) {
-        in_params.additional_params_address = voice_in.additional_params_address;
-        in_params.additional_params_size = voice_in.additional_params_size;
-        // TODO(ogniK): Reattach buffer, do we actually need to? Maybe just signal to the DSP that
-        // our context is new
-    }
-}
-
-void ServerVoiceInfo::UpdateWaveBuffers(
-    const VoiceInfo::InParams& voice_in,
-    std::array<VoiceState*, AudioCommon::MAX_CHANNEL_COUNT>& voice_states,
-    BehaviorInfo& behavior_info) {
-    if (voice_in.is_new) {
-        // Initialize our wave buffers
-        for (auto& wave_buffer : in_params.wave_buffer) {
-            wave_buffer.start_sample_offset = 0;
-            wave_buffer.end_sample_offset = 0;
-            wave_buffer.is_looping = false;
-            wave_buffer.end_of_stream = false;
-            wave_buffer.buffer_address = 0;
-            wave_buffer.buffer_size = 0;
-            wave_buffer.context_address = 0;
-            wave_buffer.context_size = 0;
-            wave_buffer.loop_start_sample = 0;
-            wave_buffer.loop_end_sample = 0;
-            wave_buffer.sent_to_dsp = true;
-        }
-
-        // Mark all our wave buffers as invalid
-        for (std::size_t channel = 0; channel < static_cast<std::size_t>(in_params.channel_count);
-             channel++) {
-            for (std::size_t i = 0; i < AudioCommon::MAX_WAVE_BUFFERS; ++i) {
-                voice_states[channel]->is_wave_buffer_valid[i] = false;
-            }
-        }
-    }
-
-    // Update our wave buffers
-    for (std::size_t i = 0; i < AudioCommon::MAX_WAVE_BUFFERS; i++) {
-        // Assume that we have at least 1 channel voice state
-        const auto have_valid_wave_buffer = voice_states[0]->is_wave_buffer_valid[i];
-
-        UpdateWaveBuffer(in_params.wave_buffer[i], voice_in.wave_buffer[i], in_params.sample_format,
-                         have_valid_wave_buffer, behavior_info);
-    }
-}
-
-void ServerVoiceInfo::UpdateWaveBuffer(ServerWaveBuffer& out_wavebuffer,
-                                       const WaveBuffer& in_wave_buffer, SampleFormat sample_format,
-                                       bool is_buffer_valid,
-                                       [[maybe_unused]] BehaviorInfo& behavior_info) {
-    if (!is_buffer_valid && out_wavebuffer.sent_to_dsp && out_wavebuffer.buffer_address != 0) {
-        out_wavebuffer.buffer_address = 0;
-        out_wavebuffer.buffer_size = 0;
-    }
-
-    if (!in_wave_buffer.sent_to_server || !in_params.buffer_mapped) {
-        // Validate sample offset sizings
-        if (sample_format == SampleFormat::Pcm16) {
-            const s64 buffer_size = static_cast<s64>(in_wave_buffer.buffer_size);
-            const s64 start = sizeof(s16) * in_wave_buffer.start_sample_offset;
-            const s64 end = sizeof(s16) * in_wave_buffer.end_sample_offset;
-            if (0 > start || start > buffer_size || 0 > end || end > buffer_size) {
-                // TODO(ogniK): Write error info
-                LOG_ERROR(Audio,
-                          "PCM16 wavebuffer has an invalid size. Buffer has size 0x{:08X}, but "
-                          "offsets were "
-                          "{:08X} - 0x{:08X}",
-                          buffer_size, sizeof(s16) * in_wave_buffer.start_sample_offset,
-                          sizeof(s16) * in_wave_buffer.end_sample_offset);
-                return;
-            }
-        } else if (sample_format == SampleFormat::Adpcm) {
-            const s64 buffer_size = static_cast<s64>(in_wave_buffer.buffer_size);
-            const s64 start_frames = in_wave_buffer.start_sample_offset / 14;
-            const s64 start_extra = in_wave_buffer.start_sample_offset % 14 == 0
-                                        ? 0
-                                        : (in_wave_buffer.start_sample_offset % 14) / 2 + 1 +
-                                              (in_wave_buffer.start_sample_offset % 2);
-            const s64 start = start_frames * 8 + start_extra;
-            const s64 end_frames = in_wave_buffer.end_sample_offset / 14;
-            const s64 end_extra = in_wave_buffer.end_sample_offset % 14 == 0
-                                      ? 0
-                                      : (in_wave_buffer.end_sample_offset % 14) / 2 + 1 +
-                                            (in_wave_buffer.end_sample_offset % 2);
-            const s64 end = end_frames * 8 + end_extra;
-            if (in_wave_buffer.start_sample_offset < 0 || start > buffer_size ||
-                in_wave_buffer.end_sample_offset < 0 || end > buffer_size) {
-                LOG_ERROR(Audio,
-                          "ADPMC wavebuffer has an invalid size. Buffer has size 0x{:08X}, but "
-                          "offsets were "
-                          "{:08X} - 0x{:08X}",
-                          in_wave_buffer.buffer_size, start, end);
-                return;
-            }
-        }
-        // TODO(ogniK): ADPCM Size error
-
-        out_wavebuffer.sent_to_dsp = false;
-        out_wavebuffer.start_sample_offset = in_wave_buffer.start_sample_offset;
-        out_wavebuffer.end_sample_offset = in_wave_buffer.end_sample_offset;
-        out_wavebuffer.is_looping = in_wave_buffer.is_looping;
-        out_wavebuffer.end_of_stream = in_wave_buffer.end_of_stream;
-
-        out_wavebuffer.buffer_address = in_wave_buffer.buffer_address;
-        out_wavebuffer.buffer_size = in_wave_buffer.buffer_size;
-        out_wavebuffer.context_address = in_wave_buffer.context_address;
-        out_wavebuffer.context_size = in_wave_buffer.context_size;
-        out_wavebuffer.loop_start_sample = in_wave_buffer.loop_start_sample;
-        out_wavebuffer.loop_end_sample = in_wave_buffer.loop_end_sample;
-        in_params.buffer_mapped =
-            in_wave_buffer.buffer_address != 0 && in_wave_buffer.buffer_size != 0;
-        // TODO(ogniK): Pool mapper attachment
-        // TODO(ogniK): IsAdpcmLoopContextBugFixed
-        if (sample_format == SampleFormat::Adpcm && in_wave_buffer.context_address != 0 &&
-            in_wave_buffer.context_size != 0 && behavior_info.IsAdpcmLoopContextBugFixed()) {
-        } else {
-            out_wavebuffer.context_address = 0;
-            out_wavebuffer.context_size = 0;
-        }
-    }
-}
-
-void ServerVoiceInfo::WriteOutStatus(
-    VoiceInfo::OutParams& voice_out, VoiceInfo::InParams& voice_in,
-    std::array<VoiceState*, AudioCommon::MAX_CHANNEL_COUNT>& voice_states) {
-    if (voice_in.is_new || in_params.is_new) {
-        in_params.is_new = true;
-        voice_out.wave_buffer_consumed = 0;
-        voice_out.played_sample_count = 0;
-        voice_out.voice_dropped = false;
-    } else {
-        const auto& state = voice_states[0];
-        voice_out.wave_buffer_consumed = state->wave_buffer_consumed;
-        voice_out.played_sample_count = state->played_sample_count;
-        voice_out.voice_dropped = state->voice_dropped;
-    }
-}
-
-const ServerVoiceInfo::InParams& ServerVoiceInfo::GetInParams() const {
-    return in_params;
-}
-
-ServerVoiceInfo::InParams& ServerVoiceInfo::GetInParams() {
-    return in_params;
-}
-
-const ServerVoiceInfo::OutParams& ServerVoiceInfo::GetOutParams() const {
-    return out_params;
-}
-
-ServerVoiceInfo::OutParams& ServerVoiceInfo::GetOutParams() {
-    return out_params;
-}
-
-bool ServerVoiceInfo::ShouldSkip() const {
-    // TODO(ogniK): Handle unmapped wave buffers or parameters
-    return !in_params.in_use || in_params.wave_buffer_count == 0 || !in_params.buffer_mapped ||
-           in_params.voice_drop_flag;
-}
-
-bool ServerVoiceInfo::UpdateForCommandGeneration(VoiceContext& voice_context) {
-    std::array<VoiceState*, AudioCommon::MAX_CHANNEL_COUNT> dsp_voice_states{};
-    if (in_params.is_new) {
-        ResetResources(voice_context);
-        in_params.last_volume = in_params.volume;
-        in_params.is_new = false;
-    }
-
-    const s32 channel_count = in_params.channel_count;
-    for (s32 i = 0; i < channel_count; i++) {
-        const auto channel_resource = in_params.voice_channel_resource_id[i];
-        dsp_voice_states[i] =
-            &voice_context.GetDspSharedState(static_cast<std::size_t>(channel_resource));
-    }
-    return UpdateParametersForCommandGeneration(dsp_voice_states);
-}
-
-void ServerVoiceInfo::ResetResources(VoiceContext& voice_context) {
-    const s32 channel_count = in_params.channel_count;
-    for (s32 i = 0; i < channel_count; i++) {
-        const auto channel_resource = in_params.voice_channel_resource_id[i];
-        auto& dsp_state =
-            voice_context.GetDspSharedState(static_cast<std::size_t>(channel_resource));
-        dsp_state = {};
-        voice_context.GetChannelResource(static_cast<std::size_t>(channel_resource))
-            .UpdateLastMixVolumes();
-    }
-}
-
-bool ServerVoiceInfo::UpdateParametersForCommandGeneration(
-    std::array<VoiceState*, AudioCommon::MAX_CHANNEL_COUNT>& dsp_voice_states) {
-    const s32 channel_count = in_params.channel_count;
-    if (in_params.wave_buffer_flush_request_count > 0) {
-        FlushWaveBuffers(in_params.wave_buffer_flush_request_count, dsp_voice_states,
-                         channel_count);
-        in_params.wave_buffer_flush_request_count = 0;
-    }
-
-    switch (in_params.current_playstate) {
-    case ServerPlayState::Play: {
-        for (std::size_t i = 0; i < AudioCommon::MAX_WAVE_BUFFERS; i++) {
-            if (!in_params.wave_buffer[i].sent_to_dsp) {
-                for (s32 channel = 0; channel < channel_count; channel++) {
-                    dsp_voice_states[channel]->is_wave_buffer_valid[i] = true;
-                }
-                in_params.wave_buffer[i].sent_to_dsp = true;
-            }
-        }
-        in_params.should_depop = false;
-        return HasValidWaveBuffer(dsp_voice_states[0]);
-    }
-    case ServerPlayState::Paused:
-    case ServerPlayState::Stop: {
-        in_params.should_depop = in_params.last_playstate == ServerPlayState::Play;
-        return in_params.should_depop;
-    }
-    case ServerPlayState::RequestStop: {
-        for (std::size_t i = 0; i < AudioCommon::MAX_WAVE_BUFFERS; i++) {
-            in_params.wave_buffer[i].sent_to_dsp = true;
-            for (s32 channel = 0; channel < channel_count; channel++) {
-                auto* dsp_state = dsp_voice_states[channel];
-
-                if (dsp_state->is_wave_buffer_valid[i]) {
-                    dsp_state->wave_buffer_index =
-                        (dsp_state->wave_buffer_index + 1) % AudioCommon::MAX_WAVE_BUFFERS;
-                    dsp_state->wave_buffer_consumed++;
-                }
-
-                dsp_state->is_wave_buffer_valid[i] = false;
-            }
-        }
-
-        for (s32 channel = 0; channel < channel_count; channel++) {
-            auto* dsp_state = dsp_voice_states[channel];
-            dsp_state->offset = 0;
-            dsp_state->played_sample_count = 0;
-            dsp_state->fraction = 0;
-            dsp_state->sample_history.fill(0);
-            dsp_state->context = {};
-        }
-
-        in_params.current_playstate = ServerPlayState::Stop;
-        in_params.should_depop = in_params.last_playstate == ServerPlayState::Play;
-        return in_params.should_depop;
-    }
-    default:
-        ASSERT_MSG(false, "Invalid playstate {}", in_params.current_playstate);
-    }
-
-    return false;
-}
-
-void ServerVoiceInfo::FlushWaveBuffers(
-    u8 flush_count, std::array<VoiceState*, AudioCommon::MAX_CHANNEL_COUNT>& dsp_voice_states,
-    s32 channel_count) {
-    auto wave_head = in_params.wave_buffer_head;
-
-    for (u8 i = 0; i < flush_count; i++) {
-        in_params.wave_buffer[wave_head].sent_to_dsp = true;
-        for (s32 channel = 0; channel < channel_count; channel++) {
-            auto* dsp_state = dsp_voice_states[channel];
-            dsp_state->wave_buffer_consumed++;
-            dsp_state->is_wave_buffer_valid[wave_head] = false;
-            dsp_state->wave_buffer_index =
-                (dsp_state->wave_buffer_index + 1) % AudioCommon::MAX_WAVE_BUFFERS;
-        }
-        wave_head = (wave_head + 1) % AudioCommon::MAX_WAVE_BUFFERS;
-    }
-}
-
-bool ServerVoiceInfo::HasValidWaveBuffer(const VoiceState* state) const {
-    const auto& valid_wb = state->is_wave_buffer_valid;
-    return std::find(valid_wb.begin(), valid_wb.end(), true) != valid_wb.end();
-}
-
-void ServerVoiceInfo::SetWaveBufferCompleted(VoiceState& dsp_state,
-                                             const ServerWaveBuffer& wave_buffer) {
-    dsp_state.is_wave_buffer_valid[dsp_state.wave_buffer_index] = false;
-    dsp_state.wave_buffer_consumed++;
-    dsp_state.wave_buffer_index = (dsp_state.wave_buffer_index + 1) % AudioCommon::MAX_WAVE_BUFFERS;
-    dsp_state.loop_count = 0;
-    if (wave_buffer.end_of_stream) {
-        dsp_state.played_sample_count = 0;
-    }
-}
-
-VoiceContext::VoiceContext(std::size_t voice_count_) : voice_count{voice_count_} {
-    for (std::size_t i = 0; i < voice_count; i++) {
-        voice_channel_resources.emplace_back(static_cast<s32>(i));
-        sorted_voice_info.push_back(&voice_info.emplace_back());
-        voice_states.emplace_back();
-        dsp_voice_states.emplace_back();
-    }
-}
-
-VoiceContext::~VoiceContext() {
-    sorted_voice_info.clear();
-}
-
-std::size_t VoiceContext::GetVoiceCount() const {
-    return voice_count;
-}
-
-ServerVoiceChannelResource& VoiceContext::GetChannelResource(std::size_t i) {
-    ASSERT(i < voice_count);
-    return voice_channel_resources.at(i);
-}
-
-const ServerVoiceChannelResource& VoiceContext::GetChannelResource(std::size_t i) const {
-    ASSERT(i < voice_count);
-    return voice_channel_resources.at(i);
-}
-
-VoiceState& VoiceContext::GetState(std::size_t i) {
-    ASSERT(i < voice_count);
-    return voice_states.at(i);
-}
-
-const VoiceState& VoiceContext::GetState(std::size_t i) const {
-    ASSERT(i < voice_count);
-    return voice_states.at(i);
-}
-
-VoiceState& VoiceContext::GetDspSharedState(std::size_t i) {
-    ASSERT(i < voice_count);
-    return dsp_voice_states.at(i);
-}
-
-const VoiceState& VoiceContext::GetDspSharedState(std::size_t i) const {
-    ASSERT(i < voice_count);
-    return dsp_voice_states.at(i);
-}
-
-ServerVoiceInfo& VoiceContext::GetInfo(std::size_t i) {
-    ASSERT(i < voice_count);
-    return voice_info.at(i);
-}
-
-const ServerVoiceInfo& VoiceContext::GetInfo(std::size_t i) const {
-    ASSERT(i < voice_count);
-    return voice_info.at(i);
-}
-
-ServerVoiceInfo& VoiceContext::GetSortedInfo(std::size_t i) {
-    ASSERT(i < voice_count);
-    return *sorted_voice_info.at(i);
-}
-
-const ServerVoiceInfo& VoiceContext::GetSortedInfo(std::size_t i) const {
-    ASSERT(i < voice_count);
-    return *sorted_voice_info.at(i);
-}
-
-s32 VoiceContext::DecodePcm16(s32* output_buffer, ServerWaveBuffer* wave_buffer, s32 channel,
-                              s32 channel_count, s32 buffer_offset, s32 sample_count,
-                              Core::Memory::Memory& memory) {
-    if (wave_buffer->buffer_address == 0) {
-        return 0;
-    }
-    if (wave_buffer->buffer_size == 0) {
-        return 0;
-    }
-    if (wave_buffer->end_sample_offset < wave_buffer->start_sample_offset) {
-        return 0;
-    }
-
-    const auto samples_remaining =
-        (wave_buffer->end_sample_offset - wave_buffer->start_sample_offset) - buffer_offset;
-    const auto start_offset = (wave_buffer->start_sample_offset + buffer_offset) * channel_count;
-    const auto buffer_pos = wave_buffer->buffer_address + start_offset;
-
-    s16* buffer_data = reinterpret_cast<s16*>(memory.GetPointer(buffer_pos));
-
-    const auto samples_processed = std::min(sample_count, samples_remaining);
-
-    // Fast path
-    if (channel_count == 1) {
-        for (std::ptrdiff_t i = 0; i < samples_processed; i++) {
-            output_buffer[i] = buffer_data[i];
-        }
-    } else {
-        for (std::ptrdiff_t i = 0; i < samples_processed; i++) {
-            output_buffer[i] = buffer_data[i * channel_count + channel];
-        }
-    }
-
-    return samples_processed;
-}
-
-void VoiceContext::SortInfo() {
-    for (std::size_t i = 0; i < voice_count; i++) {
-        sorted_voice_info[i] = &voice_info[i];
-    }
-
-    std::sort(sorted_voice_info.begin(), sorted_voice_info.end(),
-              [](const ServerVoiceInfo* lhs, const ServerVoiceInfo* rhs) {
-                  const auto& lhs_in = lhs->GetInParams();
-                  const auto& rhs_in = rhs->GetInParams();
-                  // Sort by priority
-                  if (lhs_in.priority != rhs_in.priority) {
-                      return lhs_in.priority > rhs_in.priority;
-                  } else {
-                      // If the priorities match, sort by sorting order
-                      return lhs_in.sorting_order > rhs_in.sorting_order;
-                  }
-              });
-}
-
-void VoiceContext::UpdateStateByDspShared() {
-    voice_states = dsp_voice_states;
-}
-
-} // namespace AudioCore
diff --git a/src/audio_core/voice_context.h b/src/audio_core/voice_context.h
deleted file mode 100644
index 259220dc76..0000000000
--- a/src/audio_core/voice_context.h
+++ /dev/null
@@ -1,302 +0,0 @@
-// SPDX-FileCopyrightText: Copyright 2020 yuzu Emulator Project
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#pragma once
-
-#include <array>
-#include "audio_core/algorithm/interpolate.h"
-#include "audio_core/codec.h"
-#include "audio_core/common.h"
-#include "common/bit_field.h"
-#include "common/common_funcs.h"
-#include "common/common_types.h"
-
-namespace Core::Memory {
-class Memory;
-}
-
-namespace AudioCore {
-
-class BehaviorInfo;
-class VoiceContext;
-
-enum class SampleFormat : u8 {
-    Invalid = 0,
-    Pcm8 = 1,
-    Pcm16 = 2,
-    Pcm24 = 3,
-    Pcm32 = 4,
-    PcmFloat = 5,
-    Adpcm = 6,
-};
-
-enum class PlayState : u8 {
-    Started = 0,
-    Stopped = 1,
-    Paused = 2,
-};
-
-enum class ServerPlayState {
-    Play = 0,
-    Stop = 1,
-    RequestStop = 2,
-    Paused = 3,
-};
-
-struct BiquadFilterParameter {
-    bool enabled{};
-    INSERT_PADDING_BYTES(1);
-    std::array<s16, 3> numerator{};
-    std::array<s16, 2> denominator{};
-};
-static_assert(sizeof(BiquadFilterParameter) == 0xc, "BiquadFilterParameter is an invalid size");
-
-struct WaveBuffer {
-    u64_le buffer_address{};
-    u64_le buffer_size{};
-    s32_le start_sample_offset{};
-    s32_le end_sample_offset{};
-    u8 is_looping{};
-    u8 end_of_stream{};
-    u8 sent_to_server{};
-    INSERT_PADDING_BYTES(1);
-    s32 loop_count{};
-    u64 context_address{};
-    u64 context_size{};
-    u32 loop_start_sample{};
-    u32 loop_end_sample{};
-};
-static_assert(sizeof(WaveBuffer) == 0x38, "WaveBuffer is an invalid size");
-
-struct ServerWaveBuffer {
-    VAddr buffer_address{};
-    std::size_t buffer_size{};
-    s32 start_sample_offset{};
-    s32 end_sample_offset{};
-    bool is_looping{};
-    bool end_of_stream{};
-    VAddr context_address{};
-    std::size_t context_size{};
-    s32 loop_count{};
-    u32 loop_start_sample{};
-    u32 loop_end_sample{};
-    bool sent_to_dsp{true};
-};
-
-struct BehaviorFlags {
-    BitField<0, 1, u16> is_played_samples_reset_at_loop_point;
-    BitField<1, 1, u16> is_pitch_and_src_skipped;
-};
-static_assert(sizeof(BehaviorFlags) == 0x4, "BehaviorFlags is an invalid size");
-
-struct ADPCMContext {
-    u16 header;
-    s16 yn1;
-    s16 yn2;
-};
-static_assert(sizeof(ADPCMContext) == 0x6, "ADPCMContext is an invalid size");
-
-struct VoiceState {
-    s64 played_sample_count;
-    s32 offset;
-    s32 wave_buffer_index;
-    std::array<bool, AudioCommon::MAX_WAVE_BUFFERS> is_wave_buffer_valid;
-    s32 wave_buffer_consumed;
-    std::array<s32, AudioCommon::MAX_SAMPLE_HISTORY> sample_history;
-    s32 fraction;
-    VAddr context_address;
-    Codec::ADPCM_Coeff coeff;
-    ADPCMContext context;
-    std::array<s64, 2> biquad_filter_state;
-    std::array<s32, AudioCommon::MAX_MIX_BUFFERS> previous_samples;
-    u32 external_context_size;
-    bool is_external_context_used;
-    bool voice_dropped;
-    s32 loop_count;
-};
-
-class VoiceChannelResource {
-public:
-    struct InParams {
-        s32_le id{};
-        std::array<float_le, AudioCommon::MAX_MIX_BUFFERS> mix_volume{};
-        bool in_use{};
-        INSERT_PADDING_BYTES(11);
-    };
-    static_assert(sizeof(InParams) == 0x70, "InParams is an invalid size");
-};
-
-class ServerVoiceChannelResource {
-public:
-    explicit ServerVoiceChannelResource(s32 id_);
-    ~ServerVoiceChannelResource();
-
-    bool InUse() const;
-    float GetCurrentMixVolumeAt(std::size_t i) const;
-    float GetLastMixVolumeAt(std::size_t i) const;
-    void Update(VoiceChannelResource::InParams& in_params);
-    void UpdateLastMixVolumes();
-
-    const std::array<float, AudioCommon::MAX_MIX_BUFFERS>& GetCurrentMixVolume() const;
-    const std::array<float, AudioCommon::MAX_MIX_BUFFERS>& GetLastMixVolume() const;
-
-private:
-    s32 id{};
-    std::array<float, AudioCommon::MAX_MIX_BUFFERS> mix_volume{};
-    std::array<float, AudioCommon::MAX_MIX_BUFFERS> last_mix_volume{};
-    bool in_use{};
-};
-
-class VoiceInfo {
-public:
-    struct InParams {
-        s32_le id{};
-        u32_le node_id{};
-        u8 is_new{};
-        u8 is_in_use{};
-        PlayState play_state{};
-        SampleFormat sample_format{};
-        s32_le sample_rate{};
-        s32_le priority{};
-        s32_le sorting_order{};
-        s32_le channel_count{};
-        float_le pitch{};
-        float_le volume{};
-        std::array<BiquadFilterParameter, 2> biquad_filter{};
-        s32_le wave_buffer_count{};
-        s16_le wave_buffer_head{};
-        INSERT_PADDING_BYTES(6);
-        u64_le additional_params_address{};
-        u64_le additional_params_size{};
-        s32_le mix_id{};
-        s32_le splitter_info_id{};
-        std::array<WaveBuffer, 4> wave_buffer{};
-        std::array<u32_le, 6> voice_channel_resource_ids{};
-        // TODO(ogniK): Remaining flags
-        u8 is_voice_drop_flag_clear_requested{};
-        u8 wave_buffer_flush_request_count{};
-        INSERT_PADDING_BYTES(2);
-        BehaviorFlags behavior_flags{};
-        INSERT_PADDING_BYTES(16);
-    };
-    static_assert(sizeof(InParams) == 0x170, "InParams is an invalid size");
-
-    struct OutParams {
-        u64_le played_sample_count{};
-        u32_le wave_buffer_consumed{};
-        u8 voice_dropped{};
-        INSERT_PADDING_BYTES(3);
-    };
-    static_assert(sizeof(OutParams) == 0x10, "OutParams is an invalid size");
-};
-
-class ServerVoiceInfo {
-public:
-    struct InParams {
-        bool in_use{};
-        bool is_new{};
-        bool should_depop{};
-        SampleFormat sample_format{};
-        s32 sample_rate{};
-        s32 channel_count{};
-        s32 id{};
-        s32 node_id{};
-        s32 mix_id{};
-        ServerPlayState current_playstate{};
-        ServerPlayState last_playstate{};
-        s32 priority{};
-        s32 sorting_order{};
-        float pitch{};
-        float volume{};
-        float last_volume{};
-        std::array<BiquadFilterParameter, AudioCommon::MAX_BIQUAD_FILTERS> biquad_filter{};
-        s32 wave_buffer_count{};
-        s16 wave_buffer_head{};
-        INSERT_PADDING_BYTES(2);
-        BehaviorFlags behavior_flags{};
-        VAddr additional_params_address{};
-        std::size_t additional_params_size{};
-        std::array<ServerWaveBuffer, AudioCommon::MAX_WAVE_BUFFERS> wave_buffer{};
-        std::array<s32, AudioCommon::MAX_CHANNEL_COUNT> voice_channel_resource_id{};
-        s32 splitter_info_id{};
-        u8 wave_buffer_flush_request_count{};
-        bool voice_drop_flag{};
-        bool buffer_mapped{};
-        std::array<bool, AudioCommon::MAX_BIQUAD_FILTERS> was_biquad_filter_enabled{};
-    };
-
-    struct OutParams {
-        s64 played_sample_count{};
-        s32 wave_buffer_consumed{};
-    };
-
-    ServerVoiceInfo();
-    ~ServerVoiceInfo();
-    void Initialize();
-    void UpdateParameters(const VoiceInfo::InParams& voice_in, BehaviorInfo& behavior_info);
-    void UpdateWaveBuffers(const VoiceInfo::InParams& voice_in,
-                           std::array<VoiceState*, AudioCommon::MAX_CHANNEL_COUNT>& voice_states,
-                           BehaviorInfo& behavior_info);
-    void UpdateWaveBuffer(ServerWaveBuffer& out_wavebuffer, const WaveBuffer& in_wave_buffer,
-                          SampleFormat sample_format, bool is_buffer_valid,
-                          BehaviorInfo& behavior_info);
-    void WriteOutStatus(VoiceInfo::OutParams& voice_out, VoiceInfo::InParams& voice_in,
-                        std::array<VoiceState*, AudioCommon::MAX_CHANNEL_COUNT>& voice_states);
-
-    const InParams& GetInParams() const;
-    InParams& GetInParams();
-
-    const OutParams& GetOutParams() const;
-    OutParams& GetOutParams();
-
-    bool ShouldSkip() const;
-    bool UpdateForCommandGeneration(VoiceContext& voice_context);
-    void ResetResources(VoiceContext& voice_context);
-    bool UpdateParametersForCommandGeneration(
-        std::array<VoiceState*, AudioCommon::MAX_CHANNEL_COUNT>& dsp_voice_states);
-    void FlushWaveBuffers(u8 flush_count,
-                          std::array<VoiceState*, AudioCommon::MAX_CHANNEL_COUNT>& dsp_voice_states,
-                          s32 channel_count);
-    void SetWaveBufferCompleted(VoiceState& dsp_state, const ServerWaveBuffer& wave_buffer);
-
-private:
-    std::vector<s16> stored_samples;
-    InParams in_params{};
-    OutParams out_params{};
-
-    bool HasValidWaveBuffer(const VoiceState* state) const;
-};
-
-class VoiceContext {
-public:
-    explicit VoiceContext(std::size_t voice_count_);
-    ~VoiceContext();
-
-    std::size_t GetVoiceCount() const;
-    ServerVoiceChannelResource& GetChannelResource(std::size_t i);
-    const ServerVoiceChannelResource& GetChannelResource(std::size_t i) const;
-    VoiceState& GetState(std::size_t i);
-    const VoiceState& GetState(std::size_t i) const;
-    VoiceState& GetDspSharedState(std::size_t i);
-    const VoiceState& GetDspSharedState(std::size_t i) const;
-    ServerVoiceInfo& GetInfo(std::size_t i);
-    const ServerVoiceInfo& GetInfo(std::size_t i) const;
-    ServerVoiceInfo& GetSortedInfo(std::size_t i);
-    const ServerVoiceInfo& GetSortedInfo(std::size_t i) const;
-
-    s32 DecodePcm16(s32* output_buffer, ServerWaveBuffer* wave_buffer, s32 channel,
-                    s32 channel_count, s32 buffer_offset, s32 sample_count,
-                    Core::Memory::Memory& memory);
-    void SortInfo();
-    void UpdateStateByDspShared();
-
-private:
-    std::size_t voice_count{};
-    std::vector<ServerVoiceChannelResource> voice_channel_resources{};
-    std::vector<VoiceState> voice_states{};
-    std::vector<VoiceState> dsp_voice_states{};
-    std::vector<ServerVoiceInfo> voice_info{};
-    std::vector<ServerVoiceInfo*> sorted_voice_info{};
-};
-
-} // namespace AudioCore
diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt
index 73bf626d4c..64bb753e64 100644
--- a/src/common/CMakeLists.txt
+++ b/src/common/CMakeLists.txt
@@ -43,6 +43,7 @@ add_library(common STATIC
     alignment.h
     assert.cpp
     assert.h
+    atomic_helpers.h
     atomic_ops.h
     detached_tasks.cpp
     detached_tasks.h
@@ -64,6 +65,7 @@ add_library(common STATIC
     expected.h
     fiber.cpp
     fiber.h
+    fixed_point.h
     fs/file.cpp
     fs/file.h
     fs/fs.cpp
@@ -109,6 +111,7 @@ add_library(common STATIC
     parent_of_member.h
     point.h
     quaternion.h
+    reader_writer_queue.h
     ring_buffer.h
     scm_rev.cpp
     scm_rev.h
diff --git a/src/common/atomic_helpers.h b/src/common/atomic_helpers.h
new file mode 100644
index 0000000000..6d912b52e1
--- /dev/null
+++ b/src/common/atomic_helpers.h
@@ -0,0 +1,772 @@
+// ©2013-2016 Cameron Desrochers.
+// Distributed under the simplified BSD license (see the license file that
+// should have come with this header).
+// Uses Jeff Preshing's semaphore implementation (under the terms of its
+// separate zlib license, embedded below).
+
+#pragma once
+
+// Provides portable (VC++2010+, Intel ICC 13, GCC 4.7+, and anything C++11 compliant)
+// implementation of low-level memory barriers, plus a few semi-portable utility macros (for
+// inlining and alignment). Also has a basic atomic type (limited to hardware-supported atomics with
+// no memory ordering guarantees). Uses the AE_* prefix for macros (historical reasons), and the
+// "moodycamel" namespace for symbols.
+
+#include <cassert>
+#include <cerrno>
+#include <cstdint>
+#include <ctime>
+#include <type_traits>
+
+// Platform detection
+#if defined(__INTEL_COMPILER)
+#define AE_ICC
+#elif defined(_MSC_VER)
+#define AE_VCPP
+#elif defined(__GNUC__)
+#define AE_GCC
+#endif
+
+#if defined(_M_IA64) || defined(__ia64__)
+#define AE_ARCH_IA64
+#elif defined(_WIN64) || defined(__amd64__) || defined(_M_X64) || defined(__x86_64__)
+#define AE_ARCH_X64
+#elif defined(_M_IX86) || defined(__i386__)
+#define AE_ARCH_X86
+#elif defined(_M_PPC) || defined(__powerpc__)
+#define AE_ARCH_PPC
+#else
+#define AE_ARCH_UNKNOWN
+#endif
+
+// AE_UNUSED
+#define AE_UNUSED(x) ((void)x)
+
+// AE_NO_TSAN/AE_TSAN_ANNOTATE_*
+#if defined(__has_feature)
+#if __has_feature(thread_sanitizer)
+#if __cplusplus >= 201703L // inline variables require C++17
+namespace Common {
+inline int ae_tsan_global;
+}
+#define AE_TSAN_ANNOTATE_RELEASE()                                                                 \
+    AnnotateHappensBefore(__FILE__, __LINE__, (void*)(&::moodycamel::ae_tsan_global))
+#define AE_TSAN_ANNOTATE_ACQUIRE()                                                                 \
+    AnnotateHappensAfter(__FILE__, __LINE__, (void*)(&::moodycamel::ae_tsan_global))
+extern "C" void AnnotateHappensBefore(const char*, int, void*);
+extern "C" void AnnotateHappensAfter(const char*, int, void*);
+#else // when we can't work with tsan, attempt to disable its warnings
+#define AE_NO_TSAN __attribute__((no_sanitize("thread")))
+#endif
+#endif
+#endif
+#ifndef AE_NO_TSAN
+#define AE_NO_TSAN
+#endif
+#ifndef AE_TSAN_ANNOTATE_RELEASE
+#define AE_TSAN_ANNOTATE_RELEASE()
+#define AE_TSAN_ANNOTATE_ACQUIRE()
+#endif
+
+// AE_FORCEINLINE
+#if defined(AE_VCPP) || defined(AE_ICC)
+#define AE_FORCEINLINE __forceinline
+#elif defined(AE_GCC)
+//#define AE_FORCEINLINE __attribute__((always_inline))
+#define AE_FORCEINLINE inline
+#else
+#define AE_FORCEINLINE inline
+#endif
+
+// AE_ALIGN
+#if defined(AE_VCPP) || defined(AE_ICC)
+#define AE_ALIGN(x) __declspec(align(x))
+#elif defined(AE_GCC)
+#define AE_ALIGN(x) __attribute__((aligned(x)))
+#else
+// Assume GCC compliant syntax...
+#define AE_ALIGN(x) __attribute__((aligned(x)))
+#endif
+
+// Portable atomic fences implemented below:
+
+namespace Common {
+
+enum memory_order {
+    memory_order_relaxed,
+    memory_order_acquire,
+    memory_order_release,
+    memory_order_acq_rel,
+    memory_order_seq_cst,
+
+    // memory_order_sync: Forces a full sync:
+    // #LoadLoad, #LoadStore, #StoreStore, and most significantly, #StoreLoad
+    memory_order_sync = memory_order_seq_cst
+};
+
+} // namespace Common
+
+#if (defined(AE_VCPP) && (_MSC_VER < 1700 || defined(__cplusplus_cli))) ||                         \
+    (defined(AE_ICC) && __INTEL_COMPILER < 1600)
+// VS2010 and ICC13 don't support std::atomic_*_fence, implement our own fences
+
+#include <intrin.h>
+
+#if defined(AE_ARCH_X64) || defined(AE_ARCH_X86)
+#define AeFullSync _mm_mfence
+#define AeLiteSync _mm_mfence
+#elif defined(AE_ARCH_IA64)
+#define AeFullSync __mf
+#define AeLiteSync __mf
+#elif defined(AE_ARCH_PPC)
+#include <ppcintrinsics.h>
+#define AeFullSync __sync
+#define AeLiteSync __lwsync
+#endif
+
+#ifdef AE_VCPP
+#pragma warning(push)
+#pragma warning(disable : 4365) // Disable erroneous 'conversion from long to unsigned int,
+                                // signed/unsigned mismatch' error when using `assert`
+#ifdef __cplusplus_cli
+#pragma managed(push, off)
+#endif
+#endif
+
+namespace Common {
+
+AE_FORCEINLINE void compiler_fence(memory_order order) AE_NO_TSAN {
+    switch (order) {
+    case memory_order_relaxed:
+        break;
+    case memory_order_acquire:
+        _ReadBarrier();
+        break;
+    case memory_order_release:
+        _WriteBarrier();
+        break;
+    case memory_order_acq_rel:
+        _ReadWriteBarrier();
+        break;
+    case memory_order_seq_cst:
+        _ReadWriteBarrier();
+        break;
+    default:
+        assert(false);
+    }
+}
+
+// x86/x64 have a strong memory model -- all loads and stores have
+// acquire and release semantics automatically (so only need compiler
+// barriers for those).
+#if defined(AE_ARCH_X86) || defined(AE_ARCH_X64)
+AE_FORCEINLINE void fence(memory_order order) AE_NO_TSAN {
+    switch (order) {
+    case memory_order_relaxed:
+        break;
+    case memory_order_acquire:
+        _ReadBarrier();
+        break;
+    case memory_order_release:
+        _WriteBarrier();
+        break;
+    case memory_order_acq_rel:
+        _ReadWriteBarrier();
+        break;
+    case memory_order_seq_cst:
+        _ReadWriteBarrier();
+        AeFullSync();
+        _ReadWriteBarrier();
+        break;
+    default:
+        assert(false);
+    }
+}
+#else
+AE_FORCEINLINE void fence(memory_order order) AE_NO_TSAN {
+    // Non-specialized arch, use heavier memory barriers everywhere just in case :-(
+    switch (order) {
+    case memory_order_relaxed:
+        break;
+    case memory_order_acquire:
+        _ReadBarrier();
+        AeLiteSync();
+        _ReadBarrier();
+        break;
+    case memory_order_release:
+        _WriteBarrier();
+        AeLiteSync();
+        _WriteBarrier();
+        break;
+    case memory_order_acq_rel:
+        _ReadWriteBarrier();
+        AeLiteSync();
+        _ReadWriteBarrier();
+        break;
+    case memory_order_seq_cst:
+        _ReadWriteBarrier();
+        AeFullSync();
+        _ReadWriteBarrier();
+        break;
+    default:
+        assert(false);
+    }
+}
+#endif
+} // namespace Common
+#else
+// Use standard library of atomics
+#include <atomic>
+
+namespace Common {
+
+AE_FORCEINLINE void compiler_fence(memory_order order) AE_NO_TSAN {
+    switch (order) {
+    case memory_order_relaxed:
+        break;
+    case memory_order_acquire:
+        std::atomic_signal_fence(std::memory_order_acquire);
+        break;
+    case memory_order_release:
+        std::atomic_signal_fence(std::memory_order_release);
+        break;
+    case memory_order_acq_rel:
+        std::atomic_signal_fence(std::memory_order_acq_rel);
+        break;
+    case memory_order_seq_cst:
+        std::atomic_signal_fence(std::memory_order_seq_cst);
+        break;
+    default:
+        assert(false);
+    }
+}
+
+AE_FORCEINLINE void fence(memory_order order) AE_NO_TSAN {
+    switch (order) {
+    case memory_order_relaxed:
+        break;
+    case memory_order_acquire:
+        AE_TSAN_ANNOTATE_ACQUIRE();
+        std::atomic_thread_fence(std::memory_order_acquire);
+        break;
+    case memory_order_release:
+        AE_TSAN_ANNOTATE_RELEASE();
+        std::atomic_thread_fence(std::memory_order_release);
+        break;
+    case memory_order_acq_rel:
+        AE_TSAN_ANNOTATE_ACQUIRE();
+        AE_TSAN_ANNOTATE_RELEASE();
+        std::atomic_thread_fence(std::memory_order_acq_rel);
+        break;
+    case memory_order_seq_cst:
+        AE_TSAN_ANNOTATE_ACQUIRE();
+        AE_TSAN_ANNOTATE_RELEASE();
+        std::atomic_thread_fence(std::memory_order_seq_cst);
+        break;
+    default:
+        assert(false);
+    }
+}
+
+} // namespace Common
+
+#endif
+
+#if !defined(AE_VCPP) || (_MSC_VER >= 1700 && !defined(__cplusplus_cli))
+#define AE_USE_STD_ATOMIC_FOR_WEAK_ATOMIC
+#endif
+
+#ifdef AE_USE_STD_ATOMIC_FOR_WEAK_ATOMIC
+#include <atomic>
+#endif
+#include <utility>
+
+// WARNING: *NOT* A REPLACEMENT FOR std::atomic. READ CAREFULLY:
+// Provides basic support for atomic variables -- no memory ordering guarantees are provided.
+// The guarantee of atomicity is only made for types that already have atomic load and store
+// guarantees at the hardware level -- on most platforms this generally means aligned pointers and
+// integers (only).
+namespace Common {
+template <typename T>
+class weak_atomic {
+public:
+    AE_NO_TSAN weak_atomic() : value() {}
+#ifdef AE_VCPP
+#pragma warning(push)
+#pragma warning(disable : 4100) // Get rid of (erroneous) 'unreferenced formal parameter' warning
+#endif
+    template <typename U>
+    AE_NO_TSAN weak_atomic(U&& x) : value(std::forward<U>(x)) {}
+#ifdef __cplusplus_cli
+    // Work around bug with universal reference/nullptr combination that only appears when /clr is
+    // on
+    AE_NO_TSAN weak_atomic(nullptr_t) : value(nullptr) {}
+#endif
+    AE_NO_TSAN weak_atomic(weak_atomic const& other) : value(other.load()) {}
+    AE_NO_TSAN weak_atomic(weak_atomic&& other) : value(std::move(other.load())) {}
+#ifdef AE_VCPP
+#pragma warning(pop)
+#endif
+
+    AE_FORCEINLINE operator T() const AE_NO_TSAN {
+        return load();
+    }
+
+#ifndef AE_USE_STD_ATOMIC_FOR_WEAK_ATOMIC
+    template <typename U>
+    AE_FORCEINLINE weak_atomic const& operator=(U&& x) AE_NO_TSAN {
+        value = std::forward<U>(x);
+        return *this;
+    }
+    AE_FORCEINLINE weak_atomic const& operator=(weak_atomic const& other) AE_NO_TSAN {
+        value = other.value;
+        return *this;
+    }
+
+    AE_FORCEINLINE T load() const AE_NO_TSAN {
+        return value;
+    }
+
+    AE_FORCEINLINE T fetch_add_acquire(T increment) AE_NO_TSAN {
+#if defined(AE_ARCH_X64) || defined(AE_ARCH_X86)
+        if (sizeof(T) == 4)
+            return _InterlockedExchangeAdd((long volatile*)&value, (long)increment);
+#if defined(_M_AMD64)
+        else if (sizeof(T) == 8)
+            return _InterlockedExchangeAdd64((long long volatile*)&value, (long long)increment);
+#endif
+#else
+#error Unsupported platform
+#endif
+        assert(false && "T must be either a 32 or 64 bit type");
+        return value;
+    }
+
+    AE_FORCEINLINE T fetch_add_release(T increment) AE_NO_TSAN {
+#if defined(AE_ARCH_X64) || defined(AE_ARCH_X86)
+        if (sizeof(T) == 4)
+            return _InterlockedExchangeAdd((long volatile*)&value, (long)increment);
+#if defined(_M_AMD64)
+        else if (sizeof(T) == 8)
+            return _InterlockedExchangeAdd64((long long volatile*)&value, (long long)increment);
+#endif
+#else
+#error Unsupported platform
+#endif
+        assert(false && "T must be either a 32 or 64 bit type");
+        return value;
+    }
+#else
+    template <typename U>
+    AE_FORCEINLINE weak_atomic const& operator=(U&& x) AE_NO_TSAN {
+        value.store(std::forward<U>(x), std::memory_order_relaxed);
+        return *this;
+    }
+
+    AE_FORCEINLINE weak_atomic const& operator=(weak_atomic const& other) AE_NO_TSAN {
+        value.store(other.value.load(std::memory_order_relaxed), std::memory_order_relaxed);
+        return *this;
+    }
+
+    AE_FORCEINLINE T load() const AE_NO_TSAN {
+        return value.load(std::memory_order_relaxed);
+    }
+
+    AE_FORCEINLINE T fetch_add_acquire(T increment) AE_NO_TSAN {
+        return value.fetch_add(increment, std::memory_order_acquire);
+    }
+
+    AE_FORCEINLINE T fetch_add_release(T increment) AE_NO_TSAN {
+        return value.fetch_add(increment, std::memory_order_release);
+    }
+#endif
+
+private:
+#ifndef AE_USE_STD_ATOMIC_FOR_WEAK_ATOMIC
+    // No std::atomic support, but still need to circumvent compiler optimizations.
+    // `volatile` will make memory access slow, but is guaranteed to be reliable.
+    volatile T value;
+#else
+    std::atomic<T> value;
+#endif
+};
+
+} // namespace Common
+
+// Portable single-producer, single-consumer semaphore below:
+
+#if defined(_WIN32)
+// Avoid including windows.h in a header; we only need a handful of
+// items, so we'll redeclare them here (this is relatively safe since
+// the API generally has to remain stable between Windows versions).
+// I know this is an ugly hack but it still beats polluting the global
+// namespace with thousands of generic names or adding a .cpp for nothing.
+extern "C" {
+struct _SECURITY_ATTRIBUTES;
+__declspec(dllimport) void* __stdcall CreateSemaphoreW(_SECURITY_ATTRIBUTES* lpSemaphoreAttributes,
+                                                       long lInitialCount, long lMaximumCount,
+                                                       const wchar_t* lpName);
+__declspec(dllimport) int __stdcall CloseHandle(void* hObject);
+__declspec(dllimport) unsigned long __stdcall WaitForSingleObject(void* hHandle,
+                                                                  unsigned long dwMilliseconds);
+__declspec(dllimport) int __stdcall ReleaseSemaphore(void* hSemaphore, long lReleaseCount,
+                                                     long* lpPreviousCount);
+}
+#elif defined(__MACH__)
+#include <mach/mach.h>
+#elif defined(__unix__)
+#include <semaphore.h>
+#elif defined(FREERTOS)
+#include <FreeRTOS.h>
+#include <semphr.h>
+#include <task.h>
+#endif
+
+namespace Common {
+// Code in the spsc_sema namespace below is an adaptation of Jeff Preshing's
+// portable + lightweight semaphore implementations, originally from
+// https://github.com/preshing/cpp11-on-multicore/blob/master/common/sema.h
+// LICENSE:
+// Copyright (c) 2015 Jeff Preshing
+//
+// This software is provided 'as-is', without any express or implied
+// warranty. In no event will the authors be held liable for any damages
+// arising from the use of this software.
+//
+// Permission is granted to anyone to use this software for any purpose,
+// including commercial applications, and to alter it and redistribute it
+// freely, subject to the following restrictions:
+//
+// 1. The origin of this software must not be misrepresented; you must not
+//    claim that you wrote the original software. If you use this software
+//    in a product, an acknowledgement in the product documentation would be
+//    appreciated but is not required.
+// 2. Altered source versions must be plainly marked as such, and must not be
+//    misrepresented as being the original software.
+// 3. This notice may not be removed or altered from any source distribution.
+namespace spsc_sema {
+#if defined(_WIN32)
+class Semaphore {
+private:
+    void* m_hSema;
+
+    Semaphore(const Semaphore& other);
+    Semaphore& operator=(const Semaphore& other);
+
+public:
+    AE_NO_TSAN Semaphore(int initialCount = 0) : m_hSema() {
+        assert(initialCount >= 0);
+        const long maxLong = 0x7fffffff;
+        m_hSema = CreateSemaphoreW(nullptr, initialCount, maxLong, nullptr);
+        assert(m_hSema);
+    }
+
+    AE_NO_TSAN ~Semaphore() {
+        CloseHandle(m_hSema);
+    }
+
+    bool wait() AE_NO_TSAN {
+        const unsigned long infinite = 0xffffffff;
+        return WaitForSingleObject(m_hSema, infinite) == 0;
+    }
+
+    bool try_wait() AE_NO_TSAN {
+        return WaitForSingleObject(m_hSema, 0) == 0;
+    }
+
+    bool timed_wait(std::uint64_t usecs) AE_NO_TSAN {
+        return WaitForSingleObject(m_hSema, (unsigned long)(usecs / 1000)) == 0;
+    }
+
+    void signal(int count = 1) AE_NO_TSAN {
+        while (!ReleaseSemaphore(m_hSema, count, nullptr))
+            ;
+    }
+};
+#elif defined(__MACH__)
+//---------------------------------------------------------
+// Semaphore (Apple iOS and OSX)
+// Can't use POSIX semaphores due to
+// http://lists.apple.com/archives/darwin-kernel/2009/Apr/msg00010.html
+//---------------------------------------------------------
+class Semaphore {
+private:
+    semaphore_t m_sema;
+
+    Semaphore(const Semaphore& other);
+    Semaphore& operator=(const Semaphore& other);
+
+public:
+    AE_NO_TSAN Semaphore(int initialCount = 0) : m_sema() {
+        assert(initialCount >= 0);
+        kern_return_t rc =
+            semaphore_create(mach_task_self(), &m_sema, SYNC_POLICY_FIFO, initialCount);
+        assert(rc == KERN_SUCCESS);
+        AE_UNUSED(rc);
+    }
+
+    AE_NO_TSAN ~Semaphore() {
+        semaphore_destroy(mach_task_self(), m_sema);
+    }
+
+    bool wait() AE_NO_TSAN {
+        return semaphore_wait(m_sema) == KERN_SUCCESS;
+    }
+
+    bool try_wait() AE_NO_TSAN {
+        return timed_wait(0);
+    }
+
+    bool timed_wait(std::uint64_t timeout_usecs) AE_NO_TSAN {
+        mach_timespec_t ts;
+        ts.tv_sec = static_cast<unsigned int>(timeout_usecs / 1000000);
+        ts.tv_nsec = static_cast<int>((timeout_usecs % 1000000) * 1000);
+
+        // added in OSX 10.10:
+        // https://developer.apple.com/library/prerelease/mac/documentation/General/Reference/APIDiffsMacOSX10_10SeedDiff/modules/Darwin.html
+        kern_return_t rc = semaphore_timedwait(m_sema, ts);
+        return rc == KERN_SUCCESS;
+    }
+
+    void signal() AE_NO_TSAN {
+        while (semaphore_signal(m_sema) != KERN_SUCCESS)
+            ;
+    }
+
+    void signal(int count) AE_NO_TSAN {
+        while (count-- > 0) {
+            while (semaphore_signal(m_sema) != KERN_SUCCESS)
+                ;
+        }
+    }
+};
+#elif defined(__unix__)
+//---------------------------------------------------------
+// Semaphore (POSIX, Linux)
+//---------------------------------------------------------
+class Semaphore {
+private:
+    sem_t m_sema;
+
+    Semaphore(const Semaphore& other);
+    Semaphore& operator=(const Semaphore& other);
+
+public:
+    AE_NO_TSAN Semaphore(int initialCount = 0) : m_sema() {
+        assert(initialCount >= 0);
+        int rc = sem_init(&m_sema, 0, static_cast<unsigned int>(initialCount));
+        assert(rc == 0);
+        AE_UNUSED(rc);
+    }
+
+    AE_NO_TSAN ~Semaphore() {
+        sem_destroy(&m_sema);
+    }
+
+    bool wait() AE_NO_TSAN {
+        // http://stackoverflow.com/questions/2013181/gdb-causes-sem-wait-to-fail-with-eintr-error
+        int rc;
+        do {
+            rc = sem_wait(&m_sema);
+        } while (rc == -1 && errno == EINTR);
+        return rc == 0;
+    }
+
+    bool try_wait() AE_NO_TSAN {
+        int rc;
+        do {
+            rc = sem_trywait(&m_sema);
+        } while (rc == -1 && errno == EINTR);
+        return rc == 0;
+    }
+
+    bool timed_wait(std::uint64_t usecs) AE_NO_TSAN {
+        struct timespec ts;
+        const int usecs_in_1_sec = 1000000;
+        const int nsecs_in_1_sec = 1000000000;
+        clock_gettime(CLOCK_REALTIME, &ts);
+        ts.tv_sec += static_cast<time_t>(usecs / usecs_in_1_sec);
+        ts.tv_nsec += static_cast<long>(usecs % usecs_in_1_sec) * 1000;
+        // sem_timedwait bombs if you have more than 1e9 in tv_nsec
+        // so we have to clean things up before passing it in
+        if (ts.tv_nsec >= nsecs_in_1_sec) {
+            ts.tv_nsec -= nsecs_in_1_sec;
+            ++ts.tv_sec;
+        }
+
+        int rc;
+        do {
+            rc = sem_timedwait(&m_sema, &ts);
+        } while (rc == -1 && errno == EINTR);
+        return rc == 0;
+    }
+
+    void signal() AE_NO_TSAN {
+        while (sem_post(&m_sema) == -1)
+            ;
+    }
+
+    void signal(int count) AE_NO_TSAN {
+        while (count-- > 0) {
+            while (sem_post(&m_sema) == -1)
+                ;
+        }
+    }
+};
+#elif defined(FREERTOS)
+//---------------------------------------------------------
+// Semaphore (FreeRTOS)
+//---------------------------------------------------------
+class Semaphore {
+private:
+    SemaphoreHandle_t m_sema;
+
+    Semaphore(const Semaphore& other);
+    Semaphore& operator=(const Semaphore& other);
+
+public:
+    AE_NO_TSAN Semaphore(int initialCount = 0) : m_sema() {
+        assert(initialCount >= 0);
+        m_sema = xSemaphoreCreateCounting(static_cast<UBaseType_t>(~0ull),
+                                          static_cast<UBaseType_t>(initialCount));
+        assert(m_sema);
+    }
+
+    AE_NO_TSAN ~Semaphore() {
+        vSemaphoreDelete(m_sema);
+    }
+
+    bool wait() AE_NO_TSAN {
+        return xSemaphoreTake(m_sema, portMAX_DELAY) == pdTRUE;
+    }
+
+    bool try_wait() AE_NO_TSAN {
+        // Note: In an ISR context, if this causes a task to unblock,
+        // the caller won't know about it
+        if (xPortIsInsideInterrupt())
+            return xSemaphoreTakeFromISR(m_sema, NULL) == pdTRUE;
+        return xSemaphoreTake(m_sema, 0) == pdTRUE;
+    }
+
+    bool timed_wait(std::uint64_t usecs) AE_NO_TSAN {
+        std::uint64_t msecs = usecs / 1000;
+        TickType_t ticks = static_cast<TickType_t>(msecs / portTICK_PERIOD_MS);
+        if (ticks == 0)
+            return try_wait();
+        return xSemaphoreTake(m_sema, ticks) == pdTRUE;
+    }
+
+    void signal() AE_NO_TSAN {
+        // Note: In an ISR context, if this causes a task to unblock,
+        // the caller won't know about it
+        BaseType_t rc;
+        if (xPortIsInsideInterrupt())
+            rc = xSemaphoreGiveFromISR(m_sema, NULL);
+        else
+            rc = xSemaphoreGive(m_sema);
+        assert(rc == pdTRUE);
+        AE_UNUSED(rc);
+    }
+
+    void signal(int count) AE_NO_TSAN {
+        while (count-- > 0)
+            signal();
+    }
+};
+#else
+#error Unsupported platform! (No semaphore wrapper available)
+#endif
+
+//---------------------------------------------------------
+// LightweightSemaphore
+//---------------------------------------------------------
+class LightweightSemaphore {
+public:
+    typedef std::make_signed<std::size_t>::type ssize_t;
+
+private:
+    weak_atomic<ssize_t> m_count;
+    Semaphore m_sema;
+
+    bool waitWithPartialSpinning(std::int64_t timeout_usecs = -1) AE_NO_TSAN {
+        ssize_t oldCount;
+        // Is there a better way to set the initial spin count?
+        // If we lower it to 1000, testBenaphore becomes 15x slower on my Core i7-5930K Windows PC,
+        // as threads start hitting the kernel semaphore.
+        int spin = 1024;
+        while (--spin >= 0) {
+            if (m_count.load() > 0) {
+                m_count.fetch_add_acquire(-1);
+                return true;
+            }
+            compiler_fence(memory_order_acquire); // Prevent the compiler from collapsing the loop.
+        }
+        oldCount = m_count.fetch_add_acquire(-1);
+        if (oldCount > 0)
+            return true;
+        if (timeout_usecs < 0) {
+            if (m_sema.wait())
+                return true;
+        }
+        if (timeout_usecs > 0 && m_sema.timed_wait(static_cast<uint64_t>(timeout_usecs)))
+            return true;
+        // At this point, we've timed out waiting for the semaphore, but the
+        // count is still decremented indicating we may still be waiting on
+        // it. So we have to re-adjust the count, but only if the semaphore
+        // wasn't signaled enough times for us too since then. If it was, we
+        // need to release the semaphore too.
+        while (true) {
+            oldCount = m_count.fetch_add_release(1);
+            if (oldCount < 0)
+                return false; // successfully restored things to the way they were
+            // Oh, the producer thread just signaled the semaphore after all. Try again:
+            oldCount = m_count.fetch_add_acquire(-1);
+            if (oldCount > 0 && m_sema.try_wait())
+                return true;
+        }
+    }
+
+public:
+    AE_NO_TSAN LightweightSemaphore(ssize_t initialCount = 0) : m_count(initialCount), m_sema() {
+        assert(initialCount >= 0);
+    }
+
+    bool tryWait() AE_NO_TSAN {
+        if (m_count.load() > 0) {
+            m_count.fetch_add_acquire(-1);
+            return true;
+        }
+        return false;
+    }
+
+    bool wait() AE_NO_TSAN {
+        return tryWait() || waitWithPartialSpinning();
+    }
+
+    bool wait(std::int64_t timeout_usecs) AE_NO_TSAN {
+        return tryWait() || waitWithPartialSpinning(timeout_usecs);
+    }
+
+    void signal(ssize_t count = 1) AE_NO_TSAN {
+        assert(count >= 0);
+        ssize_t oldCount = m_count.fetch_add_release(count);
+        assert(oldCount >= -1);
+        if (oldCount < 0) {
+            m_sema.signal(1);
+        }
+    }
+
+    std::size_t availableApprox() const AE_NO_TSAN {
+        ssize_t count = m_count.load();
+        return count > 0 ? static_cast<std::size_t>(count) : 0;
+    }
+};
+} // namespace spsc_sema
+} // namespace Common
+
+#if defined(AE_VCPP) && (_MSC_VER < 1700 || defined(__cplusplus_cli))
+#pragma warning(pop)
+#ifdef __cplusplus_cli
+#pragma managed(pop)
+#endif
+#endif
diff --git a/src/common/fixed_point.h b/src/common/fixed_point.h
new file mode 100644
index 0000000000..1d45e51b32
--- /dev/null
+++ b/src/common/fixed_point.h
@@ -0,0 +1,726 @@
+// From: https://github.com/eteran/cpp-utilities/blob/master/fixed/include/cpp-utilities/fixed.h
+// See also: http://stackoverflow.com/questions/79677/whats-the-best-way-to-do-fixed-point-math
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright (c) 2015 Evan Teran
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#ifndef FIXED_H_
+#define FIXED_H_
+
+#if __cplusplus >= 201402L
+#define CONSTEXPR14 constexpr
+#else
+#define CONSTEXPR14
+#endif
+
+#include <cstddef> // for size_t
+#include <cstdint>
+#include <exception>
+#include <ostream>
+#include <type_traits>
+
+namespace Common {
+
+template <size_t I, size_t F>
+class FixedPoint;
+
+namespace detail {
+
+// helper templates to make magic with types :)
+// these allow us to determine resonable types from
+// a desired size, they also let us infer the next largest type
+// from a type which is nice for the division op
+template <size_t T>
+struct type_from_size {
+    using value_type = void;
+    using unsigned_type = void;
+    using signed_type = void;
+    static constexpr bool is_specialized = false;
+};
+
+#if defined(__GNUC__) && defined(__x86_64__) && !defined(__STRICT_ANSI__)
+template <>
+struct type_from_size<128> {
+    static constexpr bool is_specialized = true;
+    static constexpr size_t size = 128;
+
+    using value_type = __int128;
+    using unsigned_type = unsigned __int128;
+    using signed_type = __int128;
+    using next_size = type_from_size<256>;
+};
+#endif
+
+template <>
+struct type_from_size<64> {
+    static constexpr bool is_specialized = true;
+    static constexpr size_t size = 64;
+
+    using value_type = int64_t;
+    using unsigned_type = std::make_unsigned<value_type>::type;
+    using signed_type = std::make_signed<value_type>::type;
+    using next_size = type_from_size<128>;
+};
+
+template <>
+struct type_from_size<32> {
+    static constexpr bool is_specialized = true;
+    static constexpr size_t size = 32;
+
+    using value_type = int32_t;
+    using unsigned_type = std::make_unsigned<value_type>::type;
+    using signed_type = std::make_signed<value_type>::type;
+    using next_size = type_from_size<64>;
+};
+
+template <>
+struct type_from_size<16> {
+    static constexpr bool is_specialized = true;
+    static constexpr size_t size = 16;
+
+    using value_type = int16_t;
+    using unsigned_type = std::make_unsigned<value_type>::type;
+    using signed_type = std::make_signed<value_type>::type;
+    using next_size = type_from_size<32>;
+};
+
+template <>
+struct type_from_size<8> {
+    static constexpr bool is_specialized = true;
+    static constexpr size_t size = 8;
+
+    using value_type = int8_t;
+    using unsigned_type = std::make_unsigned<value_type>::type;
+    using signed_type = std::make_signed<value_type>::type;
+    using next_size = type_from_size<16>;
+};
+
+// this is to assist in adding support for non-native base
+// types (for adding big-int support), this should be fine
+// unless your bit-int class doesn't nicely support casting
+template <class B, class N>
+constexpr B next_to_base(N rhs) {
+    return static_cast<B>(rhs);
+}
+
+struct divide_by_zero : std::exception {};
+
+template <size_t I, size_t F>
+CONSTEXPR14 FixedPoint<I, F> divide(
+    FixedPoint<I, F> numerator, FixedPoint<I, F> denominator, FixedPoint<I, F>& remainder,
+    typename std::enable_if<type_from_size<I + F>::next_size::is_specialized>::type* = nullptr) {
+
+    using next_type = typename FixedPoint<I, F>::next_type;
+    using base_type = typename FixedPoint<I, F>::base_type;
+    constexpr size_t fractional_bits = FixedPoint<I, F>::fractional_bits;
+
+    next_type t(numerator.to_raw());
+    t <<= fractional_bits;
+
+    FixedPoint<I, F> quotient;
+
+    quotient = FixedPoint<I, F>::from_base(next_to_base<base_type>(t / denominator.to_raw()));
+    remainder = FixedPoint<I, F>::from_base(next_to_base<base_type>(t % denominator.to_raw()));
+
+    return quotient;
+}
+
+template <size_t I, size_t F>
+CONSTEXPR14 FixedPoint<I, F> divide(
+    FixedPoint<I, F> numerator, FixedPoint<I, F> denominator, FixedPoint<I, F>& remainder,
+    typename std::enable_if<!type_from_size<I + F>::next_size::is_specialized>::type* = nullptr) {
+
+    using unsigned_type = typename FixedPoint<I, F>::unsigned_type;
+
+    constexpr int bits = FixedPoint<I, F>::total_bits;
+
+    if (denominator == 0) {
+        throw divide_by_zero();
+    } else {
+
+        int sign = 0;
+
+        FixedPoint<I, F> quotient;
+
+        if (numerator < 0) {
+            sign ^= 1;
+            numerator = -numerator;
+        }
+
+        if (denominator < 0) {
+            sign ^= 1;
+            denominator = -denominator;
+        }
+
+        unsigned_type n = numerator.to_raw();
+        unsigned_type d = denominator.to_raw();
+        unsigned_type x = 1;
+        unsigned_type answer = 0;
+
+        // egyptian division algorithm
+        while ((n >= d) && (((d >> (bits - 1)) & 1) == 0)) {
+            x <<= 1;
+            d <<= 1;
+        }
+
+        while (x != 0) {
+            if (n >= d) {
+                n -= d;
+                answer += x;
+            }
+
+            x >>= 1;
+            d >>= 1;
+        }
+
+        unsigned_type l1 = n;
+        unsigned_type l2 = denominator.to_raw();
+
+        // calculate the lower bits (needs to be unsigned)
+        while (l1 >> (bits - F) > 0) {
+            l1 >>= 1;
+            l2 >>= 1;
+        }
+        const unsigned_type lo = (l1 << F) / l2;
+
+        quotient = FixedPoint<I, F>::from_base((answer << F) | lo);
+        remainder = n;
+
+        if (sign) {
+            quotient = -quotient;
+        }
+
+        return quotient;
+    }
+}
+
+// this is the usual implementation of multiplication
+template <size_t I, size_t F>
+CONSTEXPR14 FixedPoint<I, F> multiply(
+    FixedPoint<I, F> lhs, FixedPoint<I, F> rhs,
+    typename std::enable_if<type_from_size<I + F>::next_size::is_specialized>::type* = nullptr) {
+
+    using next_type = typename FixedPoint<I, F>::next_type;
+    using base_type = typename FixedPoint<I, F>::base_type;
+
+    constexpr size_t fractional_bits = FixedPoint<I, F>::fractional_bits;
+
+    next_type t(static_cast<next_type>(lhs.to_raw()) * static_cast<next_type>(rhs.to_raw()));
+    t >>= fractional_bits;
+
+    return FixedPoint<I, F>::from_base(next_to_base<base_type>(t));
+}
+
+// this is the fall back version we use when we don't have a next size
+// it is slightly slower, but is more robust since it doesn't
+// require and upgraded type
+template <size_t I, size_t F>
+CONSTEXPR14 FixedPoint<I, F> multiply(
+    FixedPoint<I, F> lhs, FixedPoint<I, F> rhs,
+    typename std::enable_if<!type_from_size<I + F>::next_size::is_specialized>::type* = nullptr) {
+
+    using base_type = typename FixedPoint<I, F>::base_type;
+
+    constexpr size_t fractional_bits = FixedPoint<I, F>::fractional_bits;
+    constexpr base_type integer_mask = FixedPoint<I, F>::integer_mask;
+    constexpr base_type fractional_mask = FixedPoint<I, F>::fractional_mask;
+
+    // more costly but doesn't need a larger type
+    const base_type a_hi = (lhs.to_raw() & integer_mask) >> fractional_bits;
+    const base_type b_hi = (rhs.to_raw() & integer_mask) >> fractional_bits;
+    const base_type a_lo = (lhs.to_raw() & fractional_mask);
+    const base_type b_lo = (rhs.to_raw() & fractional_mask);
+
+    const base_type x1 = a_hi * b_hi;
+    const base_type x2 = a_hi * b_lo;
+    const base_type x3 = a_lo * b_hi;
+    const base_type x4 = a_lo * b_lo;
+
+    return FixedPoint<I, F>::from_base((x1 << fractional_bits) + (x3 + x2) +
+                                       (x4 >> fractional_bits));
+}
+} // namespace detail
+
+template <size_t I, size_t F>
+class FixedPoint {
+    static_assert(detail::type_from_size<I + F>::is_specialized, "invalid combination of sizes");
+
+public:
+    static constexpr size_t fractional_bits = F;
+    static constexpr size_t integer_bits = I;
+    static constexpr size_t total_bits = I + F;
+
+    using base_type_info = detail::type_from_size<total_bits>;
+
+    using base_type = typename base_type_info::value_type;
+    using next_type = typename base_type_info::next_size::value_type;
+    using unsigned_type = typename base_type_info::unsigned_type;
+
+public:
+#ifdef __GNUC__
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Woverflow"
+#endif
+    static constexpr base_type fractional_mask =
+        ~(static_cast<unsigned_type>(~base_type(0)) << fractional_bits);
+    static constexpr base_type integer_mask = ~fractional_mask;
+#ifdef __GNUC__
+#pragma GCC diagnostic pop
+#endif
+
+public:
+    static constexpr base_type one = base_type(1) << fractional_bits;
+
+public: // constructors
+    FixedPoint() = default;
+    FixedPoint(const FixedPoint&) = default;
+    FixedPoint(FixedPoint&&) = default;
+    FixedPoint& operator=(const FixedPoint&) = default;
+
+    template <class Number>
+    constexpr FixedPoint(
+        Number n, typename std::enable_if<std::is_arithmetic<Number>::value>::type* = nullptr)
+        : data_(static_cast<base_type>(n * one)) {}
+
+public: // conversion
+    template <size_t I2, size_t F2>
+    CONSTEXPR14 explicit FixedPoint(FixedPoint<I2, F2> other) {
+        static_assert(I2 <= I && F2 <= F, "Scaling conversion can only upgrade types");
+        using T = FixedPoint<I2, F2>;
+
+        const base_type fractional = (other.data_ & T::fractional_mask);
+        const base_type integer = (other.data_ & T::integer_mask) >> T::fractional_bits;
+        data_ =
+            (integer << fractional_bits) | (fractional << (fractional_bits - T::fractional_bits));
+    }
+
+private:
+    // this makes it simpler to create a FixedPoint point object from
+    // a native type without scaling
+    // use "FixedPoint::from_base" in order to perform this.
+    struct NoScale {};
+
+    constexpr FixedPoint(base_type n, const NoScale&) : data_(n) {}
+
+public:
+    static constexpr FixedPoint from_base(base_type n) {
+        return FixedPoint(n, NoScale());
+    }
+
+public: // comparison operators
+    constexpr bool operator==(FixedPoint rhs) const {
+        return data_ == rhs.data_;
+    }
+
+    constexpr bool operator!=(FixedPoint rhs) const {
+        return data_ != rhs.data_;
+    }
+
+    constexpr bool operator<(FixedPoint rhs) const {
+        return data_ < rhs.data_;
+    }
+
+    constexpr bool operator>(FixedPoint rhs) const {
+        return data_ > rhs.data_;
+    }
+
+    constexpr bool operator<=(FixedPoint rhs) const {
+        return data_ <= rhs.data_;
+    }
+
+    constexpr bool operator>=(FixedPoint rhs) const {
+        return data_ >= rhs.data_;
+    }
+
+public: // unary operators
+    constexpr bool operator!() const {
+        return !data_;
+    }
+
+    constexpr FixedPoint operator~() const {
+        // NOTE(eteran): this will often appear to "just negate" the value
+        // that is not an error, it is because -x == (~x+1)
+        // and that "+1" is adding an infinitesimally small fraction to the
+        // complimented value
+        return FixedPoint::from_base(~data_);
+    }
+
+    constexpr FixedPoint operator-() const {
+        return FixedPoint::from_base(-data_);
+    }
+
+    constexpr FixedPoint operator+() const {
+        return FixedPoint::from_base(+data_);
+    }
+
+    CONSTEXPR14 FixedPoint& operator++() {
+        data_ += one;
+        return *this;
+    }
+
+    CONSTEXPR14 FixedPoint& operator--() {
+        data_ -= one;
+        return *this;
+    }
+
+    CONSTEXPR14 FixedPoint operator++(int) {
+        FixedPoint tmp(*this);
+        data_ += one;
+        return tmp;
+    }
+
+    CONSTEXPR14 FixedPoint operator--(int) {
+        FixedPoint tmp(*this);
+        data_ -= one;
+        return tmp;
+    }
+
+public: // basic math operators
+    CONSTEXPR14 FixedPoint& operator+=(FixedPoint n) {
+        data_ += n.data_;
+        return *this;
+    }
+
+    CONSTEXPR14 FixedPoint& operator-=(FixedPoint n) {
+        data_ -= n.data_;
+        return *this;
+    }
+
+    CONSTEXPR14 FixedPoint& operator*=(FixedPoint n) {
+        return assign(detail::multiply(*this, n));
+    }
+
+    CONSTEXPR14 FixedPoint& operator/=(FixedPoint n) {
+        FixedPoint temp;
+        return assign(detail::divide(*this, n, temp));
+    }
+
+private:
+    CONSTEXPR14 FixedPoint& assign(FixedPoint rhs) {
+        data_ = rhs.data_;
+        return *this;
+    }
+
+public: // binary math operators, effects underlying bit pattern since these
+        // don't really typically make sense for non-integer values
+    CONSTEXPR14 FixedPoint& operator&=(FixedPoint n) {
+        data_ &= n.data_;
+        return *this;
+    }
+
+    CONSTEXPR14 FixedPoint& operator|=(FixedPoint n) {
+        data_ |= n.data_;
+        return *this;
+    }
+
+    CONSTEXPR14 FixedPoint& operator^=(FixedPoint n) {
+        data_ ^= n.data_;
+        return *this;
+    }
+
+    template <class Integer,
+              class = typename std::enable_if<std::is_integral<Integer>::value>::type>
+    CONSTEXPR14 FixedPoint& operator>>=(Integer n) {
+        data_ >>= n;
+        return *this;
+    }
+
+    template <class Integer,
+              class = typename std::enable_if<std::is_integral<Integer>::value>::type>
+    CONSTEXPR14 FixedPoint& operator<<=(Integer n) {
+        data_ <<= n;
+        return *this;
+    }
+
+public: // conversion to basic types
+    constexpr void round_up() {
+        data_ += (data_ & fractional_mask) >> 1;
+    }
+
+    constexpr int to_int() {
+        round_up();
+        return static_cast<int>((data_ & integer_mask) >> fractional_bits);
+    }
+
+    constexpr unsigned int to_uint() const {
+        round_up();
+        return static_cast<unsigned int>((data_ & integer_mask) >> fractional_bits);
+    }
+
+    constexpr int64_t to_long() {
+        round_up();
+        return static_cast<int64_t>((data_ & integer_mask) >> fractional_bits);
+    }
+
+    constexpr int to_int_floor() const {
+        return static_cast<int>((data_ & integer_mask) >> fractional_bits);
+    }
+
+    constexpr int64_t to_long_floor() {
+        return static_cast<int64_t>((data_ & integer_mask) >> fractional_bits);
+    }
+
+    constexpr unsigned int to_uint_floor() const {
+        return static_cast<unsigned int>((data_ & integer_mask) >> fractional_bits);
+    }
+
+    constexpr float to_float() const {
+        return static_cast<float>(data_) / FixedPoint::one;
+    }
+
+    constexpr double to_double() const {
+        return static_cast<double>(data_) / FixedPoint::one;
+    }
+
+    constexpr base_type to_raw() const {
+        return data_;
+    }
+
+    constexpr void clear_int() {
+        data_ &= fractional_mask;
+    }
+
+    constexpr base_type get_frac() const {
+        return data_ & fractional_mask;
+    }
+
+public:
+    CONSTEXPR14 void swap(FixedPoint& rhs) {
+        using std::swap;
+        swap(data_, rhs.data_);
+    }
+
+public:
+    base_type data_;
+};
+
+// if we have the same fractional portion, but differing integer portions, we trivially upgrade the
+// smaller type
+template <size_t I1, size_t I2, size_t F>
+CONSTEXPR14 typename std::conditional<I1 >= I2, FixedPoint<I1, F>, FixedPoint<I2, F>>::type
+operator+(FixedPoint<I1, F> lhs, FixedPoint<I2, F> rhs) {
+
+    using T = typename std::conditional<I1 >= I2, FixedPoint<I1, F>, FixedPoint<I2, F>>::type;
+
+    const T l = T::from_base(lhs.to_raw());
+    const T r = T::from_base(rhs.to_raw());
+    return l + r;
+}
+
+template <size_t I1, size_t I2, size_t F>
+CONSTEXPR14 typename std::conditional<I1 >= I2, FixedPoint<I1, F>, FixedPoint<I2, F>>::type
+operator-(FixedPoint<I1, F> lhs, FixedPoint<I2, F> rhs) {
+
+    using T = typename std::conditional<I1 >= I2, FixedPoint<I1, F>, FixedPoint<I2, F>>::type;
+
+    const T l = T::from_base(lhs.to_raw());
+    const T r = T::from_base(rhs.to_raw());
+    return l - r;
+}
+
+template <size_t I1, size_t I2, size_t F>
+CONSTEXPR14 typename std::conditional<I1 >= I2, FixedPoint<I1, F>, FixedPoint<I2, F>>::type
+operator*(FixedPoint<I1, F> lhs, FixedPoint<I2, F> rhs) {
+
+    using T = typename std::conditional<I1 >= I2, FixedPoint<I1, F>, FixedPoint<I2, F>>::type;
+
+    const T l = T::from_base(lhs.to_raw());
+    const T r = T::from_base(rhs.to_raw());
+    return l * r;
+}
+
+template <size_t I1, size_t I2, size_t F>
+CONSTEXPR14 typename std::conditional<I1 >= I2, FixedPoint<I1, F>, FixedPoint<I2, F>>::type
+operator/(FixedPoint<I1, F> lhs, FixedPoint<I2, F> rhs) {
+
+    using T = typename std::conditional<I1 >= I2, FixedPoint<I1, F>, FixedPoint<I2, F>>::type;
+
+    const T l = T::from_base(lhs.to_raw());
+    const T r = T::from_base(rhs.to_raw());
+    return l / r;
+}
+
+template <size_t I, size_t F>
+std::ostream& operator<<(std::ostream& os, FixedPoint<I, F> f) {
+    os << f.to_double();
+    return os;
+}
+
+// basic math operators
+template <size_t I, size_t F>
+CONSTEXPR14 FixedPoint<I, F> operator+(FixedPoint<I, F> lhs, FixedPoint<I, F> rhs) {
+    lhs += rhs;
+    return lhs;
+}
+template <size_t I, size_t F>
+CONSTEXPR14 FixedPoint<I, F> operator-(FixedPoint<I, F> lhs, FixedPoint<I, F> rhs) {
+    lhs -= rhs;
+    return lhs;
+}
+template <size_t I, size_t F>
+CONSTEXPR14 FixedPoint<I, F> operator*(FixedPoint<I, F> lhs, FixedPoint<I, F> rhs) {
+    lhs *= rhs;
+    return lhs;
+}
+template <size_t I, size_t F>
+CONSTEXPR14 FixedPoint<I, F> operator/(FixedPoint<I, F> lhs, FixedPoint<I, F> rhs) {
+    lhs /= rhs;
+    return lhs;
+}
+
+template <size_t I, size_t F, class Number,
+          class = typename std::enable_if<std::is_arithmetic<Number>::value>::type>
+CONSTEXPR14 FixedPoint<I, F> operator+(FixedPoint<I, F> lhs, Number rhs) {
+    lhs += FixedPoint<I, F>(rhs);
+    return lhs;
+}
+template <size_t I, size_t F, class Number,
+          class = typename std::enable_if<std::is_arithmetic<Number>::value>::type>
+CONSTEXPR14 FixedPoint<I, F> operator-(FixedPoint<I, F> lhs, Number rhs) {
+    lhs -= FixedPoint<I, F>(rhs);
+    return lhs;
+}
+template <size_t I, size_t F, class Number,
+          class = typename std::enable_if<std::is_arithmetic<Number>::value>::type>
+CONSTEXPR14 FixedPoint<I, F> operator*(FixedPoint<I, F> lhs, Number rhs) {
+    lhs *= FixedPoint<I, F>(rhs);
+    return lhs;
+}
+template <size_t I, size_t F, class Number,
+          class = typename std::enable_if<std::is_arithmetic<Number>::value>::type>
+CONSTEXPR14 FixedPoint<I, F> operator/(FixedPoint<I, F> lhs, Number rhs) {
+    lhs /= FixedPoint<I, F>(rhs);
+    return lhs;
+}
+
+template <size_t I, size_t F, class Number,
+          class = typename std::enable_if<std::is_arithmetic<Number>::value>::type>
+CONSTEXPR14 FixedPoint<I, F> operator+(Number lhs, FixedPoint<I, F> rhs) {
+    FixedPoint<I, F> tmp(lhs);
+    tmp += rhs;
+    return tmp;
+}
+template <size_t I, size_t F, class Number,
+          class = typename std::enable_if<std::is_arithmetic<Number>::value>::type>
+CONSTEXPR14 FixedPoint<I, F> operator-(Number lhs, FixedPoint<I, F> rhs) {
+    FixedPoint<I, F> tmp(lhs);
+    tmp -= rhs;
+    return tmp;
+}
+template <size_t I, size_t F, class Number,
+          class = typename std::enable_if<std::is_arithmetic<Number>::value>::type>
+CONSTEXPR14 FixedPoint<I, F> operator*(Number lhs, FixedPoint<I, F> rhs) {
+    FixedPoint<I, F> tmp(lhs);
+    tmp *= rhs;
+    return tmp;
+}
+template <size_t I, size_t F, class Number,
+          class = typename std::enable_if<std::is_arithmetic<Number>::value>::type>
+CONSTEXPR14 FixedPoint<I, F> operator/(Number lhs, FixedPoint<I, F> rhs) {
+    FixedPoint<I, F> tmp(lhs);
+    tmp /= rhs;
+    return tmp;
+}
+
+// shift operators
+template <size_t I, size_t F, class Integer,
+          class = typename std::enable_if<std::is_integral<Integer>::value>::type>
+CONSTEXPR14 FixedPoint<I, F> operator<<(FixedPoint<I, F> lhs, Integer rhs) {
+    lhs <<= rhs;
+    return lhs;
+}
+template <size_t I, size_t F, class Integer,
+          class = typename std::enable_if<std::is_integral<Integer>::value>::type>
+CONSTEXPR14 FixedPoint<I, F> operator>>(FixedPoint<I, F> lhs, Integer rhs) {
+    lhs >>= rhs;
+    return lhs;
+}
+
+// comparison operators
+template <size_t I, size_t F, class Number,
+          class = typename std::enable_if<std::is_arithmetic<Number>::value>::type>
+constexpr bool operator>(FixedPoint<I, F> lhs, Number rhs) {
+    return lhs > FixedPoint<I, F>(rhs);
+}
+template <size_t I, size_t F, class Number,
+          class = typename std::enable_if<std::is_arithmetic<Number>::value>::type>
+constexpr bool operator<(FixedPoint<I, F> lhs, Number rhs) {
+    return lhs < FixedPoint<I, F>(rhs);
+}
+template <size_t I, size_t F, class Number,
+          class = typename std::enable_if<std::is_arithmetic<Number>::value>::type>
+constexpr bool operator>=(FixedPoint<I, F> lhs, Number rhs) {
+    return lhs >= FixedPoint<I, F>(rhs);
+}
+template <size_t I, size_t F, class Number,
+          class = typename std::enable_if<std::is_arithmetic<Number>::value>::type>
+constexpr bool operator<=(FixedPoint<I, F> lhs, Number rhs) {
+    return lhs <= FixedPoint<I, F>(rhs);
+}
+template <size_t I, size_t F, class Number,
+          class = typename std::enable_if<std::is_arithmetic<Number>::value>::type>
+constexpr bool operator==(FixedPoint<I, F> lhs, Number rhs) {
+    return lhs == FixedPoint<I, F>(rhs);
+}
+template <size_t I, size_t F, class Number,
+          class = typename std::enable_if<std::is_arithmetic<Number>::value>::type>
+constexpr bool operator!=(FixedPoint<I, F> lhs, Number rhs) {
+    return lhs != FixedPoint<I, F>(rhs);
+}
+
+template <size_t I, size_t F, class Number,
+          class = typename std::enable_if<std::is_arithmetic<Number>::value>::type>
+constexpr bool operator>(Number lhs, FixedPoint<I, F> rhs) {
+    return FixedPoint<I, F>(lhs) > rhs;
+}
+template <size_t I, size_t F, class Number,
+          class = typename std::enable_if<std::is_arithmetic<Number>::value>::type>
+constexpr bool operator<(Number lhs, FixedPoint<I, F> rhs) {
+    return FixedPoint<I, F>(lhs) < rhs;
+}
+template <size_t I, size_t F, class Number,
+          class = typename std::enable_if<std::is_arithmetic<Number>::value>::type>
+constexpr bool operator>=(Number lhs, FixedPoint<I, F> rhs) {
+    return FixedPoint<I, F>(lhs) >= rhs;
+}
+template <size_t I, size_t F, class Number,
+          class = typename std::enable_if<std::is_arithmetic<Number>::value>::type>
+constexpr bool operator<=(Number lhs, FixedPoint<I, F> rhs) {
+    return FixedPoint<I, F>(lhs) <= rhs;
+}
+template <size_t I, size_t F, class Number,
+          class = typename std::enable_if<std::is_arithmetic<Number>::value>::type>
+constexpr bool operator==(Number lhs, FixedPoint<I, F> rhs) {
+    return FixedPoint<I, F>(lhs) == rhs;
+}
+template <size_t I, size_t F, class Number,
+          class = typename std::enable_if<std::is_arithmetic<Number>::value>::type>
+constexpr bool operator!=(Number lhs, FixedPoint<I, F> rhs) {
+    return FixedPoint<I, F>(lhs) != rhs;
+}
+
+} // namespace Common
+
+#undef CONSTEXPR14
+
+#endif
diff --git a/src/common/reader_writer_queue.h b/src/common/reader_writer_queue.h
new file mode 100644
index 0000000000..8d2c9408cb
--- /dev/null
+++ b/src/common/reader_writer_queue.h
@@ -0,0 +1,941 @@
+// ©2013-2020 Cameron Desrochers.
+// Distributed under the simplified BSD license (see the license file that
+// should have come with this header).
+
+#pragma once
+
+#include <cassert>
+#include <cstdint>
+#include <cstdlib> // For malloc/free/abort & size_t
+#include <memory>
+#include <new>
+#include <stdexcept>
+#include <type_traits>
+#include <utility>
+
+#include "common/atomic_helpers.h"
+
+#if __cplusplus > 199711L || _MSC_VER >= 1700 // C++11 or VS2012
+#include <chrono>
+#endif
+
+// A lock-free queue for a single-consumer, single-producer architecture.
+// The queue is also wait-free in the common path (except if more memory
+// needs to be allocated, in which case malloc is called).
+// Allocates memory sparingly, and only once if the original maximum size
+// estimate is never exceeded.
+// Tested on x86/x64 processors, but semantics should be correct for all
+// architectures (given the right implementations in atomicops.h), provided
+// that aligned integer and pointer accesses are naturally atomic.
+// Note that there should only be one consumer thread and producer thread;
+// Switching roles of the threads, or using multiple consecutive threads for
+// one role, is not safe unless properly synchronized.
+// Using the queue exclusively from one thread is fine, though a bit silly.
+
+#ifndef MOODYCAMEL_CACHE_LINE_SIZE
+#define MOODYCAMEL_CACHE_LINE_SIZE 64
+#endif
+
+#ifndef MOODYCAMEL_EXCEPTIONS_ENABLED
+#if (defined(_MSC_VER) && defined(_CPPUNWIND)) || (defined(__GNUC__) && defined(__EXCEPTIONS)) ||  \
+    (!defined(_MSC_VER) && !defined(__GNUC__))
+#define MOODYCAMEL_EXCEPTIONS_ENABLED
+#endif
+#endif
+
+#ifndef MOODYCAMEL_HAS_EMPLACE
+#if !defined(_MSC_VER) ||                                                                          \
+    _MSC_VER >= 1800 // variadic templates: either a non-MS compiler or VS >= 2013
+#define MOODYCAMEL_HAS_EMPLACE 1
+#endif
+#endif
+
+#ifndef MOODYCAMEL_MAYBE_ALIGN_TO_CACHELINE
+#if defined(__APPLE__) && defined(__MACH__) && __cplusplus >= 201703L
+// This is required to find out what deployment target we are using
+#include <CoreFoundation/CoreFoundation.h>
+#if !defined(MAC_OS_X_VERSION_MIN_REQUIRED) ||                                                     \
+    MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_14
+// C++17 new(size_t, align_val_t) is not backwards-compatible with older versions of macOS, so we
+// can't support over-alignment in this case
+#define MOODYCAMEL_MAYBE_ALIGN_TO_CACHELINE
+#endif
+#endif
+#endif
+
+#ifndef MOODYCAMEL_MAYBE_ALIGN_TO_CACHELINE
+#define MOODYCAMEL_MAYBE_ALIGN_TO_CACHELINE AE_ALIGN(MOODYCAMEL_CACHE_LINE_SIZE)
+#endif
+
+#ifdef AE_VCPP
+#pragma warning(push)
+#pragma warning(disable : 4324) // structure was padded due to __declspec(align())
+#pragma warning(disable : 4820) // padding was added
+#pragma warning(disable : 4127) // conditional expression is constant
+#endif
+
+namespace Common {
+
+template <typename T, size_t MAX_BLOCK_SIZE = 512>
+class MOODYCAMEL_MAYBE_ALIGN_TO_CACHELINE ReaderWriterQueue {
+    // Design: Based on a queue-of-queues. The low-level queues are just
+    // circular buffers with front and tail indices indicating where the
+    // next element to dequeue is and where the next element can be enqueued,
+    // respectively. Each low-level queue is called a "block". Each block
+    // wastes exactly one element's worth of space to keep the design simple
+    // (if front == tail then the queue is empty, and can't be full).
+    // The high-level queue is a circular linked list of blocks; again there
+    // is a front and tail, but this time they are pointers to the blocks.
+    // The front block is where the next element to be dequeued is, provided
+    // the block is not empty. The back block is where elements are to be
+    // enqueued, provided the block is not full.
+    // The producer thread owns all the tail indices/pointers. The consumer
+    // thread owns all the front indices/pointers. Both threads read each
+    // other's variables, but only the owning thread updates them. E.g. After
+    // the consumer reads the producer's tail, the tail may change before the
+    // consumer is done dequeuing an object, but the consumer knows the tail
+    // will never go backwards, only forwards.
+    // If there is no room to enqueue an object, an additional block (of
+    // equal size to the last block) is added. Blocks are never removed.
+
+public:
+    typedef T value_type;
+
+    // Constructs a queue that can hold at least `size` elements without further
+    // allocations. If more than MAX_BLOCK_SIZE elements are requested,
+    // then several blocks of MAX_BLOCK_SIZE each are reserved (including
+    // at least one extra buffer block).
+    AE_NO_TSAN explicit ReaderWriterQueue(size_t size = 15)
+#ifndef NDEBUG
+        : enqueuing(false), dequeuing(false)
+#endif
+    {
+        assert(MAX_BLOCK_SIZE == ceilToPow2(MAX_BLOCK_SIZE) &&
+               "MAX_BLOCK_SIZE must be a power of 2");
+        assert(MAX_BLOCK_SIZE >= 2 && "MAX_BLOCK_SIZE must be at least 2");
+
+        Block* firstBlock = nullptr;
+
+        largestBlockSize =
+            ceilToPow2(size + 1); // We need a spare slot to fit size elements in the block
+        if (largestBlockSize > MAX_BLOCK_SIZE * 2) {
+            // We need a spare block in case the producer is writing to a different block the
+            // consumer is reading from, and wants to enqueue the maximum number of elements. We
+            // also need a spare element in each block to avoid the ambiguity between front == tail
+            // meaning "empty" and "full". So the effective number of slots that are guaranteed to
+            // be usable at any time is the block size - 1 times the number of blocks - 1. Solving
+            // for size and applying a ceiling to the division gives us (after simplifying):
+            size_t initialBlockCount = (size + MAX_BLOCK_SIZE * 2 - 3) / (MAX_BLOCK_SIZE - 1);
+            largestBlockSize = MAX_BLOCK_SIZE;
+            Block* lastBlock = nullptr;
+            for (size_t i = 0; i != initialBlockCount; ++i) {
+                auto block = make_block(largestBlockSize);
+                if (block == nullptr) {
+#ifdef MOODYCAMEL_EXCEPTIONS_ENABLED
+                    throw std::bad_alloc();
+#else
+                    abort();
+#endif
+                }
+                if (firstBlock == nullptr) {
+                    firstBlock = block;
+                } else {
+                    lastBlock->next = block;
+                }
+                lastBlock = block;
+                block->next = firstBlock;
+            }
+        } else {
+            firstBlock = make_block(largestBlockSize);
+            if (firstBlock == nullptr) {
+#ifdef MOODYCAMEL_EXCEPTIONS_ENABLED
+                throw std::bad_alloc();
+#else
+                abort();
+#endif
+            }
+            firstBlock->next = firstBlock;
+        }
+        frontBlock = firstBlock;
+        tailBlock = firstBlock;
+
+        // Make sure the reader/writer threads will have the initialized memory setup above:
+        fence(memory_order_sync);
+    }
+
+    // Note: The queue should not be accessed concurrently while it's
+    // being moved. It's up to the user to synchronize this.
+    AE_NO_TSAN ReaderWriterQueue(ReaderWriterQueue&& other)
+        : frontBlock(other.frontBlock.load()), tailBlock(other.tailBlock.load()),
+          largestBlockSize(other.largestBlockSize)
+#ifndef NDEBUG
+          ,
+          enqueuing(false), dequeuing(false)
+#endif
+    {
+        other.largestBlockSize = 32;
+        Block* b = other.make_block(other.largestBlockSize);
+        if (b == nullptr) {
+#ifdef MOODYCAMEL_EXCEPTIONS_ENABLED
+            throw std::bad_alloc();
+#else
+            abort();
+#endif
+        }
+        b->next = b;
+        other.frontBlock = b;
+        other.tailBlock = b;
+    }
+
+    // Note: The queue should not be accessed concurrently while it's
+    // being moved. It's up to the user to synchronize this.
+    ReaderWriterQueue& operator=(ReaderWriterQueue&& other) AE_NO_TSAN {
+        Block* b = frontBlock.load();
+        frontBlock = other.frontBlock.load();
+        other.frontBlock = b;
+        b = tailBlock.load();
+        tailBlock = other.tailBlock.load();
+        other.tailBlock = b;
+        std::swap(largestBlockSize, other.largestBlockSize);
+        return *this;
+    }
+
+    // Note: The queue should not be accessed concurrently while it's
+    // being deleted. It's up to the user to synchronize this.
+    AE_NO_TSAN ~ReaderWriterQueue() {
+        // Make sure we get the latest version of all variables from other CPUs:
+        fence(memory_order_sync);
+
+        // Destroy any remaining objects in queue and free memory
+        Block* frontBlock_ = frontBlock;
+        Block* block = frontBlock_;
+        do {
+            Block* nextBlock = block->next;
+            size_t blockFront = block->front;
+            size_t blockTail = block->tail;
+
+            for (size_t i = blockFront; i != blockTail; i = (i + 1) & block->sizeMask) {
+                auto element = reinterpret_cast<T*>(block->data + i * sizeof(T));
+                element->~T();
+                (void)element;
+            }
+
+            auto rawBlock = block->rawThis;
+            block->~Block();
+            std::free(rawBlock);
+            block = nextBlock;
+        } while (block != frontBlock_);
+    }
+
+    // Enqueues a copy of element if there is room in the queue.
+    // Returns true if the element was enqueued, false otherwise.
+    // Does not allocate memory.
+    AE_FORCEINLINE bool try_enqueue(T const& element) AE_NO_TSAN {
+        return inner_enqueue<CannotAlloc>(element);
+    }
+
+    // Enqueues a moved copy of element if there is room in the queue.
+    // Returns true if the element was enqueued, false otherwise.
+    // Does not allocate memory.
+    AE_FORCEINLINE bool try_enqueue(T&& element) AE_NO_TSAN {
+        return inner_enqueue<CannotAlloc>(std::forward<T>(element));
+    }
+
+#if MOODYCAMEL_HAS_EMPLACE
+    // Like try_enqueue() but with emplace semantics (i.e. construct-in-place).
+    template <typename... Args>
+    AE_FORCEINLINE bool try_emplace(Args&&... args) AE_NO_TSAN {
+        return inner_enqueue<CannotAlloc>(std::forward<Args>(args)...);
+    }
+#endif
+
+    // Enqueues a copy of element on the queue.
+    // Allocates an additional block of memory if needed.
+    // Only fails (returns false) if memory allocation fails.
+    AE_FORCEINLINE bool enqueue(T const& element) AE_NO_TSAN {
+        return inner_enqueue<CanAlloc>(element);
+    }
+
+    // Enqueues a moved copy of element on the queue.
+    // Allocates an additional block of memory if needed.
+    // Only fails (returns false) if memory allocation fails.
+    AE_FORCEINLINE bool enqueue(T&& element) AE_NO_TSAN {
+        return inner_enqueue<CanAlloc>(std::forward<T>(element));
+    }
+
+#if MOODYCAMEL_HAS_EMPLACE
+    // Like enqueue() but with emplace semantics (i.e. construct-in-place).
+    template <typename... Args>
+    AE_FORCEINLINE bool emplace(Args&&... args) AE_NO_TSAN {
+        return inner_enqueue<CanAlloc>(std::forward<Args>(args)...);
+    }
+#endif
+
+    // Attempts to dequeue an element; if the queue is empty,
+    // returns false instead. If the queue has at least one element,
+    // moves front to result using operator=, then returns true.
+    template <typename U>
+    bool try_dequeue(U& result) AE_NO_TSAN {
+#ifndef NDEBUG
+        ReentrantGuard guard(this->dequeuing);
+#endif
+
+        // High-level pseudocode:
+        // Remember where the tail block is
+        // If the front block has an element in it, dequeue it
+        // Else
+        //     If front block was the tail block when we entered the function, return false
+        //     Else advance to next block and dequeue the item there
+
+        // Note that we have to use the value of the tail block from before we check if the front
+        // block is full or not, in case the front block is empty and then, before we check if the
+        // tail block is at the front block or not, the producer fills up the front block *and
+        // moves on*, which would make us skip a filled block. Seems unlikely, but was consistently
+        // reproducible in practice.
+        // In order to avoid overhead in the common case, though, we do a double-checked pattern
+        // where we have the fast path if the front block is not empty, then read the tail block,
+        // then re-read the front block and check if it's not empty again, then check if the tail
+        // block has advanced.
+
+        Block* frontBlock_ = frontBlock.load();
+        size_t blockTail = frontBlock_->localTail;
+        size_t blockFront = frontBlock_->front.load();
+
+        if (blockFront != blockTail ||
+            blockFront != (frontBlock_->localTail = frontBlock_->tail.load())) {
+            fence(memory_order_acquire);
+
+        non_empty_front_block:
+            // Front block not empty, dequeue from here
+            auto element = reinterpret_cast<T*>(frontBlock_->data + blockFront * sizeof(T));
+            result = std::move(*element);
+            element->~T();
+
+            blockFront = (blockFront + 1) & frontBlock_->sizeMask;
+
+            fence(memory_order_release);
+            frontBlock_->front = blockFront;
+        } else if (frontBlock_ != tailBlock.load()) {
+            fence(memory_order_acquire);
+
+            frontBlock_ = frontBlock.load();
+            blockTail = frontBlock_->localTail = frontBlock_->tail.load();
+            blockFront = frontBlock_->front.load();
+            fence(memory_order_acquire);
+
+            if (blockFront != blockTail) {
+                // Oh look, the front block isn't empty after all
+                goto non_empty_front_block;
+            }
+
+            // Front block is empty but there's another block ahead, advance to it
+            Block* nextBlock = frontBlock_->next;
+            // Don't need an acquire fence here since next can only ever be set on the tailBlock,
+            // and we're not the tailBlock, and we did an acquire earlier after reading tailBlock
+            // which ensures next is up-to-date on this CPU in case we recently were at tailBlock.
+
+            size_t nextBlockFront = nextBlock->front.load();
+            size_t nextBlockTail = nextBlock->localTail = nextBlock->tail.load();
+            fence(memory_order_acquire);
+
+            // Since the tailBlock is only ever advanced after being written to,
+            // we know there's for sure an element to dequeue on it
+            assert(nextBlockFront != nextBlockTail);
+            AE_UNUSED(nextBlockTail);
+
+            // We're done with this block, let the producer use it if it needs
+            fence(memory_order_release); // Expose possibly pending changes to frontBlock->front
+                                         // from last dequeue
+            frontBlock = frontBlock_ = nextBlock;
+
+            compiler_fence(memory_order_release); // Not strictly needed
+
+            auto element = reinterpret_cast<T*>(frontBlock_->data + nextBlockFront * sizeof(T));
+
+            result = std::move(*element);
+            element->~T();
+
+            nextBlockFront = (nextBlockFront + 1) & frontBlock_->sizeMask;
+
+            fence(memory_order_release);
+            frontBlock_->front = nextBlockFront;
+        } else {
+            // No elements in current block and no other block to advance to
+            return false;
+        }
+
+        return true;
+    }
+
+    // Returns a pointer to the front element in the queue (the one that
+    // would be removed next by a call to `try_dequeue` or `pop`). If the
+    // queue appears empty at the time the method is called, nullptr is
+    // returned instead.
+    // Must be called only from the consumer thread.
+    T* peek() const AE_NO_TSAN {
+#ifndef NDEBUG
+        ReentrantGuard guard(this->dequeuing);
+#endif
+        // See try_dequeue() for reasoning
+
+        Block* frontBlock_ = frontBlock.load();
+        size_t blockTail = frontBlock_->localTail;
+        size_t blockFront = frontBlock_->front.load();
+
+        if (blockFront != blockTail ||
+            blockFront != (frontBlock_->localTail = frontBlock_->tail.load())) {
+            fence(memory_order_acquire);
+        non_empty_front_block:
+            return reinterpret_cast<T*>(frontBlock_->data + blockFront * sizeof(T));
+        } else if (frontBlock_ != tailBlock.load()) {
+            fence(memory_order_acquire);
+            frontBlock_ = frontBlock.load();
+            blockTail = frontBlock_->localTail = frontBlock_->tail.load();
+            blockFront = frontBlock_->front.load();
+            fence(memory_order_acquire);
+
+            if (blockFront != blockTail) {
+                goto non_empty_front_block;
+            }
+
+            Block* nextBlock = frontBlock_->next;
+
+            size_t nextBlockFront = nextBlock->front.load();
+            fence(memory_order_acquire);
+
+            assert(nextBlockFront != nextBlock->tail.load());
+            return reinterpret_cast<T*>(nextBlock->data + nextBlockFront * sizeof(T));
+        }
+
+        return nullptr;
+    }
+
+    // Removes the front element from the queue, if any, without returning it.
+    // Returns true on success, or false if the queue appeared empty at the time
+    // `pop` was called.
+    bool pop() AE_NO_TSAN {
+#ifndef NDEBUG
+        ReentrantGuard guard(this->dequeuing);
+#endif
+        // See try_dequeue() for reasoning
+
+        Block* frontBlock_ = frontBlock.load();
+        size_t blockTail = frontBlock_->localTail;
+        size_t blockFront = frontBlock_->front.load();
+
+        if (blockFront != blockTail ||
+            blockFront != (frontBlock_->localTail = frontBlock_->tail.load())) {
+            fence(memory_order_acquire);
+
+        non_empty_front_block:
+            auto element = reinterpret_cast<T*>(frontBlock_->data + blockFront * sizeof(T));
+            element->~T();
+
+            blockFront = (blockFront + 1) & frontBlock_->sizeMask;
+
+            fence(memory_order_release);
+            frontBlock_->front = blockFront;
+        } else if (frontBlock_ != tailBlock.load()) {
+            fence(memory_order_acquire);
+            frontBlock_ = frontBlock.load();
+            blockTail = frontBlock_->localTail = frontBlock_->tail.load();
+            blockFront = frontBlock_->front.load();
+            fence(memory_order_acquire);
+
+            if (blockFront != blockTail) {
+                goto non_empty_front_block;
+            }
+
+            // Front block is empty but there's another block ahead, advance to it
+            Block* nextBlock = frontBlock_->next;
+
+            size_t nextBlockFront = nextBlock->front.load();
+            size_t nextBlockTail = nextBlock->localTail = nextBlock->tail.load();
+            fence(memory_order_acquire);
+
+            assert(nextBlockFront != nextBlockTail);
+            AE_UNUSED(nextBlockTail);
+
+            fence(memory_order_release);
+            frontBlock = frontBlock_ = nextBlock;
+
+            compiler_fence(memory_order_release);
+
+            auto element = reinterpret_cast<T*>(frontBlock_->data + nextBlockFront * sizeof(T));
+            element->~T();
+
+            nextBlockFront = (nextBlockFront + 1) & frontBlock_->sizeMask;
+
+            fence(memory_order_release);
+            frontBlock_->front = nextBlockFront;
+        } else {
+            // No elements in current block and no other block to advance to
+            return false;
+        }
+
+        return true;
+    }
+
+    // Returns the approximate number of items currently in the queue.
+    // Safe to call from both the producer and consumer threads.
+    inline size_t size_approx() const AE_NO_TSAN {
+        size_t result = 0;
+        Block* frontBlock_ = frontBlock.load();
+        Block* block = frontBlock_;
+        do {
+            fence(memory_order_acquire);
+            size_t blockFront = block->front.load();
+            size_t blockTail = block->tail.load();
+            result += (blockTail - blockFront) & block->sizeMask;
+            block = block->next.load();
+        } while (block != frontBlock_);
+        return result;
+    }
+
+    // Returns the total number of items that could be enqueued without incurring
+    // an allocation when this queue is empty.
+    // Safe to call from both the producer and consumer threads.
+    //
+    // NOTE: The actual capacity during usage may be different depending on the consumer.
+    //       If the consumer is removing elements concurrently, the producer cannot add to
+    //       the block the consumer is removing from until it's completely empty, except in
+    //       the case where the producer was writing to the same block the consumer was
+    //       reading from the whole time.
+    inline size_t max_capacity() const {
+        size_t result = 0;
+        Block* frontBlock_ = frontBlock.load();
+        Block* block = frontBlock_;
+        do {
+            fence(memory_order_acquire);
+            result += block->sizeMask;
+            block = block->next.load();
+        } while (block != frontBlock_);
+        return result;
+    }
+
+private:
+    enum AllocationMode { CanAlloc, CannotAlloc };
+
+#if MOODYCAMEL_HAS_EMPLACE
+    template <AllocationMode canAlloc, typename... Args>
+    bool inner_enqueue(Args&&... args) AE_NO_TSAN
+#else
+    template <AllocationMode canAlloc, typename U>
+    bool inner_enqueue(U&& element) AE_NO_TSAN
+#endif
+    {
+#ifndef NDEBUG
+        ReentrantGuard guard(this->enqueuing);
+#endif
+
+        // High-level pseudocode (assuming we're allowed to alloc a new block):
+        // If room in tail block, add to tail
+        // Else check next block
+        //     If next block is not the head block, enqueue on next block
+        //     Else create a new block and enqueue there
+        //     Advance tail to the block we just enqueued to
+
+        Block* tailBlock_ = tailBlock.load();
+        size_t blockFront = tailBlock_->localFront;
+        size_t blockTail = tailBlock_->tail.load();
+
+        size_t nextBlockTail = (blockTail + 1) & tailBlock_->sizeMask;
+        if (nextBlockTail != blockFront ||
+            nextBlockTail != (tailBlock_->localFront = tailBlock_->front.load())) {
+            fence(memory_order_acquire);
+            // This block has room for at least one more element
+            char* location = tailBlock_->data + blockTail * sizeof(T);
+#if MOODYCAMEL_HAS_EMPLACE
+            new (location) T(std::forward<Args>(args)...);
+#else
+            new (location) T(std::forward<U>(element));
+#endif
+
+            fence(memory_order_release);
+            tailBlock_->tail = nextBlockTail;
+        } else {
+            fence(memory_order_acquire);
+            if (tailBlock_->next.load() != frontBlock) {
+                // Note that the reason we can't advance to the frontBlock and start adding new
+                // entries there is because if we did, then dequeue would stay in that block,
+                // eventually reading the new values, instead of advancing to the next full block
+                // (whose values were enqueued first and so should be consumed first).
+
+                fence(memory_order_acquire); // Ensure we get latest writes if we got the latest
+                                             // frontBlock
+
+                // tailBlock is full, but there's a free block ahead, use it
+                Block* tailBlockNext = tailBlock_->next.load();
+                size_t nextBlockFront = tailBlockNext->localFront = tailBlockNext->front.load();
+                nextBlockTail = tailBlockNext->tail.load();
+                fence(memory_order_acquire);
+
+                // This block must be empty since it's not the head block and we
+                // go through the blocks in a circle
+                assert(nextBlockFront == nextBlockTail);
+                tailBlockNext->localFront = nextBlockFront;
+
+                char* location = tailBlockNext->data + nextBlockTail * sizeof(T);
+#if MOODYCAMEL_HAS_EMPLACE
+                new (location) T(std::forward<Args>(args)...);
+#else
+                new (location) T(std::forward<U>(element));
+#endif
+
+                tailBlockNext->tail = (nextBlockTail + 1) & tailBlockNext->sizeMask;
+
+                fence(memory_order_release);
+                tailBlock = tailBlockNext;
+            } else if (canAlloc == CanAlloc) {
+                // tailBlock is full and there's no free block ahead; create a new block
+                auto newBlockSize =
+                    largestBlockSize >= MAX_BLOCK_SIZE ? largestBlockSize : largestBlockSize * 2;
+                auto newBlock = make_block(newBlockSize);
+                if (newBlock == nullptr) {
+                    // Could not allocate a block!
+                    return false;
+                }
+                largestBlockSize = newBlockSize;
+
+#if MOODYCAMEL_HAS_EMPLACE
+                new (newBlock->data) T(std::forward<Args>(args)...);
+#else
+                new (newBlock->data) T(std::forward<U>(element));
+#endif
+                assert(newBlock->front == 0);
+                newBlock->tail = newBlock->localTail = 1;
+
+                newBlock->next = tailBlock_->next.load();
+                tailBlock_->next = newBlock;
+
+                // Might be possible for the dequeue thread to see the new tailBlock->next
+                // *without* seeing the new tailBlock value, but this is OK since it can't
+                // advance to the next block until tailBlock is set anyway (because the only
+                // case where it could try to read the next is if it's already at the tailBlock,
+                // and it won't advance past tailBlock in any circumstance).
+
+                fence(memory_order_release);
+                tailBlock = newBlock;
+            } else if (canAlloc == CannotAlloc) {
+                // Would have had to allocate a new block to enqueue, but not allowed
+                return false;
+            } else {
+                assert(false && "Should be unreachable code");
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    // Disable copying
+    ReaderWriterQueue(ReaderWriterQueue const&) {}
+
+    // Disable assignment
+    ReaderWriterQueue& operator=(ReaderWriterQueue const&) {}
+
+    AE_FORCEINLINE static size_t ceilToPow2(size_t x) {
+        // From http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
+        --x;
+        x |= x >> 1;
+        x |= x >> 2;
+        x |= x >> 4;
+        for (size_t i = 1; i < sizeof(size_t); i <<= 1) {
+            x |= x >> (i << 3);
+        }
+        ++x;
+        return x;
+    }
+
+    template <typename U>
+    static AE_FORCEINLINE char* align_for(char* ptr) AE_NO_TSAN {
+        const std::size_t alignment = std::alignment_of<U>::value;
+        return ptr + (alignment - (reinterpret_cast<std::uintptr_t>(ptr) % alignment)) % alignment;
+    }
+
+private:
+#ifndef NDEBUG
+    struct ReentrantGuard {
+        AE_NO_TSAN ReentrantGuard(weak_atomic<bool>& _inSection) : inSection(_inSection) {
+            assert(!inSection &&
+                   "Concurrent (or re-entrant) enqueue or dequeue operation detected (only one "
+                   "thread at a time may hold the producer or consumer role)");
+            inSection = true;
+        }
+
+        AE_NO_TSAN ~ReentrantGuard() {
+            inSection = false;
+        }
+
+    private:
+        ReentrantGuard& operator=(ReentrantGuard const&);
+
+    private:
+        weak_atomic<bool>& inSection;
+    };
+#endif
+
+    struct Block {
+        // Avoid false-sharing by putting highly contended variables on their own cache lines
+        weak_atomic<size_t> front; // (Atomic) Elements are read from here
+        size_t localTail;          // An uncontended shadow copy of tail, owned by the consumer
+
+        char cachelineFiller0[MOODYCAMEL_CACHE_LINE_SIZE - sizeof(weak_atomic<size_t>) -
+                              sizeof(size_t)];
+        weak_atomic<size_t> tail; // (Atomic) Elements are enqueued here
+        size_t localFront;
+
+        char cachelineFiller1[MOODYCAMEL_CACHE_LINE_SIZE - sizeof(weak_atomic<size_t>) -
+                              sizeof(size_t)]; // next isn't very contended, but we don't want it on
+                                               // the same cache line as tail (which is)
+        weak_atomic<Block*> next;              // (Atomic)
+
+        char* data; // Contents (on heap) are aligned to T's alignment
+
+        const size_t sizeMask;
+
+        // size must be a power of two (and greater than 0)
+        AE_NO_TSAN Block(size_t const& _size, char* _rawThis, char* _data)
+            : front(0UL), localTail(0), tail(0UL), localFront(0), next(nullptr), data(_data),
+              sizeMask(_size - 1), rawThis(_rawThis) {}
+
+    private:
+        // C4512 - Assignment operator could not be generated
+        Block& operator=(Block const&);
+
+    public:
+        char* rawThis;
+    };
+
+    static Block* make_block(size_t capacity) AE_NO_TSAN {
+        // Allocate enough memory for the block itself, as well as all the elements it will contain
+        auto size = sizeof(Block) + std::alignment_of<Block>::value - 1;
+        size += sizeof(T) * capacity + std::alignment_of<T>::value - 1;
+        auto newBlockRaw = static_cast<char*>(std::malloc(size));
+        if (newBlockRaw == nullptr) {
+            return nullptr;
+        }
+
+        auto newBlockAligned = align_for<Block>(newBlockRaw);
+        auto newBlockData = align_for<T>(newBlockAligned + sizeof(Block));
+        return new (newBlockAligned) Block(capacity, newBlockRaw, newBlockData);
+    }
+
+private:
+    weak_atomic<Block*> frontBlock; // (Atomic) Elements are dequeued from this block
+
+    char cachelineFiller[MOODYCAMEL_CACHE_LINE_SIZE - sizeof(weak_atomic<Block*>)];
+    weak_atomic<Block*> tailBlock; // (Atomic) Elements are enqueued to this block
+
+    size_t largestBlockSize;
+
+#ifndef NDEBUG
+    weak_atomic<bool> enqueuing;
+    mutable weak_atomic<bool> dequeuing;
+#endif
+};
+
+// Like ReaderWriterQueue, but also providees blocking operations
+template <typename T, size_t MAX_BLOCK_SIZE = 512>
+class BlockingReaderWriterQueue {
+private:
+    typedef ::Common::ReaderWriterQueue<T, MAX_BLOCK_SIZE> ReaderWriterQueue;
+
+public:
+    explicit BlockingReaderWriterQueue(size_t size = 15) AE_NO_TSAN
+        : inner(size),
+          sema(new spsc_sema::LightweightSemaphore()) {}
+
+    BlockingReaderWriterQueue(BlockingReaderWriterQueue&& other) AE_NO_TSAN
+        : inner(std::move(other.inner)),
+          sema(std::move(other.sema)) {}
+
+    BlockingReaderWriterQueue& operator=(BlockingReaderWriterQueue&& other) AE_NO_TSAN {
+        std::swap(sema, other.sema);
+        std::swap(inner, other.inner);
+        return *this;
+    }
+
+    // Enqueues a copy of element if there is room in the queue.
+    // Returns true if the element was enqueued, false otherwise.
+    // Does not allocate memory.
+    AE_FORCEINLINE bool try_enqueue(T const& element) AE_NO_TSAN {
+        if (inner.try_enqueue(element)) {
+            sema->signal();
+            return true;
+        }
+        return false;
+    }
+
+    // Enqueues a moved copy of element if there is room in the queue.
+    // Returns true if the element was enqueued, false otherwise.
+    // Does not allocate memory.
+    AE_FORCEINLINE bool try_enqueue(T&& element) AE_NO_TSAN {
+        if (inner.try_enqueue(std::forward<T>(element))) {
+            sema->signal();
+            return true;
+        }
+        return false;
+    }
+
+#if MOODYCAMEL_HAS_EMPLACE
+    // Like try_enqueue() but with emplace semantics (i.e. construct-in-place).
+    template <typename... Args>
+    AE_FORCEINLINE bool try_emplace(Args&&... args) AE_NO_TSAN {
+        if (inner.try_emplace(std::forward<Args>(args)...)) {
+            sema->signal();
+            return true;
+        }
+        return false;
+    }
+#endif
+
+    // Enqueues a copy of element on the queue.
+    // Allocates an additional block of memory if needed.
+    // Only fails (returns false) if memory allocation fails.
+    AE_FORCEINLINE bool enqueue(T const& element) AE_NO_TSAN {
+        if (inner.enqueue(element)) {
+            sema->signal();
+            return true;
+        }
+        return false;
+    }
+
+    // Enqueues a moved copy of element on the queue.
+    // Allocates an additional block of memory if needed.
+    // Only fails (returns false) if memory allocation fails.
+    AE_FORCEINLINE bool enqueue(T&& element) AE_NO_TSAN {
+        if (inner.enqueue(std::forward<T>(element))) {
+            sema->signal();
+            return true;
+        }
+        return false;
+    }
+
+#if MOODYCAMEL_HAS_EMPLACE
+    // Like enqueue() but with emplace semantics (i.e. construct-in-place).
+    template <typename... Args>
+    AE_FORCEINLINE bool emplace(Args&&... args) AE_NO_TSAN {
+        if (inner.emplace(std::forward<Args>(args)...)) {
+            sema->signal();
+            return true;
+        }
+        return false;
+    }
+#endif
+
+    // Attempts to dequeue an element; if the queue is empty,
+    // returns false instead. If the queue has at least one element,
+    // moves front to result using operator=, then returns true.
+    template <typename U>
+    bool try_dequeue(U& result) AE_NO_TSAN {
+        if (sema->tryWait()) {
+            bool success = inner.try_dequeue(result);
+            assert(success);
+            AE_UNUSED(success);
+            return true;
+        }
+        return false;
+    }
+
+    // Attempts to dequeue an element; if the queue is empty,
+    // waits until an element is available, then dequeues it.
+    template <typename U>
+    void wait_dequeue(U& result) AE_NO_TSAN {
+        while (!sema->wait())
+            ;
+        bool success = inner.try_dequeue(result);
+        AE_UNUSED(result);
+        assert(success);
+        AE_UNUSED(success);
+    }
+
+    // Attempts to dequeue an element; if the queue is empty,
+    // waits until an element is available up to the specified timeout,
+    // then dequeues it and returns true, or returns false if the timeout
+    // expires before an element can be dequeued.
+    // Using a negative timeout indicates an indefinite timeout,
+    // and is thus functionally equivalent to calling wait_dequeue.
+    template <typename U>
+    bool wait_dequeue_timed(U& result, std::int64_t timeout_usecs) AE_NO_TSAN {
+        if (!sema->wait(timeout_usecs)) {
+            return false;
+        }
+        bool success = inner.try_dequeue(result);
+        AE_UNUSED(result);
+        assert(success);
+        AE_UNUSED(success);
+        return true;
+    }
+
+#if __cplusplus > 199711L || _MSC_VER >= 1700
+    // Attempts to dequeue an element; if the queue is empty,
+    // waits until an element is available up to the specified timeout,
+    // then dequeues it and returns true, or returns false if the timeout
+    // expires before an element can be dequeued.
+    // Using a negative timeout indicates an indefinite timeout,
+    // and is thus functionally equivalent to calling wait_dequeue.
+    template <typename U, typename Rep, typename Period>
+    inline bool wait_dequeue_timed(U& result,
+                                   std::chrono::duration<Rep, Period> const& timeout) AE_NO_TSAN {
+        return wait_dequeue_timed(
+            result, std::chrono::duration_cast<std::chrono::microseconds>(timeout).count());
+    }
+#endif
+
+    // Returns a pointer to the front element in the queue (the one that
+    // would be removed next by a call to `try_dequeue` or `pop`). If the
+    // queue appears empty at the time the method is called, nullptr is
+    // returned instead.
+    // Must be called only from the consumer thread.
+    AE_FORCEINLINE T* peek() const AE_NO_TSAN {
+        return inner.peek();
+    }
+
+    // Removes the front element from the queue, if any, without returning it.
+    // Returns true on success, or false if the queue appeared empty at the time
+    // `pop` was called.
+    AE_FORCEINLINE bool pop() AE_NO_TSAN {
+        if (sema->tryWait()) {
+            bool result = inner.pop();
+            assert(result);
+            AE_UNUSED(result);
+            return true;
+        }
+        return false;
+    }
+
+    // Returns the approximate number of items currently in the queue.
+    // Safe to call from both the producer and consumer threads.
+    AE_FORCEINLINE size_t size_approx() const AE_NO_TSAN {
+        return sema->availableApprox();
+    }
+
+    // Returns the total number of items that could be enqueued without incurring
+    // an allocation when this queue is empty.
+    // Safe to call from both the producer and consumer threads.
+    //
+    // NOTE: The actual capacity during usage may be different depending on the consumer.
+    //       If the consumer is removing elements concurrently, the producer cannot add to
+    //       the block the consumer is removing from until it's completely empty, except in
+    //       the case where the producer was writing to the same block the consumer was
+    //       reading from the whole time.
+    AE_FORCEINLINE size_t max_capacity() const {
+        return inner.max_capacity();
+    }
+
+private:
+    // Disable copying & assignment
+    BlockingReaderWriterQueue(BlockingReaderWriterQueue const&) {}
+    BlockingReaderWriterQueue& operator=(BlockingReaderWriterQueue const&) {}
+
+private:
+    ReaderWriterQueue inner;
+    std::unique_ptr<spsc_sema::LightweightSemaphore> sema;
+};
+
+} // namespace Common
+
+#ifdef AE_VCPP
+#pragma warning(pop)
+#endif
diff --git a/src/common/settings.cpp b/src/common/settings.cpp
index d4c52989a9..1c7b6dfae6 100644
--- a/src/common/settings.cpp
+++ b/src/common/settings.cpp
@@ -62,7 +62,8 @@ void LogSettings() {
     log_setting("Renderer_UseAsynchronousShaders", values.use_asynchronous_shaders.GetValue());
     log_setting("Renderer_AnisotropicFilteringLevel", values.max_anisotropy.GetValue());
     log_setting("Audio_OutputEngine", values.sink_id.GetValue());
-    log_setting("Audio_OutputDevice", values.audio_device_id.GetValue());
+    log_setting("Audio_OutputDevice", values.audio_output_device_id.GetValue());
+    log_setting("Audio_InputDevice", values.audio_input_device_id.GetValue());
     log_setting("DataStorage_UseVirtualSd", values.use_virtual_sd.GetValue());
     log_path("DataStorage_CacheDir", Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir));
     log_path("DataStorage_ConfigDir", Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir));
diff --git a/src/common/settings.h b/src/common/settings.h
index 2bccb8642c..06d72c8bff 100644
--- a/src/common/settings.h
+++ b/src/common/settings.h
@@ -370,10 +370,12 @@ struct TouchFromButtonMap {
 
 struct Values {
     // Audio
-    Setting<std::string> audio_device_id{"auto", "output_device"};
     Setting<std::string> sink_id{"auto", "output_engine"};
+    Setting<std::string> audio_output_device_id{"auto", "output_device"};
+    Setting<std::string> audio_input_device_id{"auto", "input_device"};
     Setting<bool> audio_muted{false, "audio_muted"};
     SwitchableSetting<u8, true> volume{100, 0, 100, "volume"};
+    Setting<bool> dump_audio_commands{false, "dump_audio_commands"};
 
     // Core
     SwitchableSetting<bool> use_multi_core{true, "use_multi_core"};
diff --git a/src/core/core.cpp b/src/core/core.cpp
index 7723d9782a..0ede0d85c0 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -8,6 +8,7 @@
 #include <memory>
 #include <utility>
 
+#include "audio_core/audio_core.h"
 #include "common/fs/fs.h"
 #include "common/logging/log.h"
 #include "common/microprofile.h"
@@ -140,6 +141,8 @@ struct System::Impl {
         core_timing.SyncPause(false);
         is_paused = false;
 
+        audio_core->PauseSinks(false);
+
         return status;
     }
 
@@ -147,6 +150,8 @@ struct System::Impl {
         std::unique_lock<std::mutex> lk(suspend_guard);
         status = SystemResultStatus::Success;
 
+        audio_core->PauseSinks(true);
+
         core_timing.SyncPause(true);
         kernel.Suspend(true);
         is_paused = true;
@@ -154,6 +159,11 @@ struct System::Impl {
         return status;
     }
 
+    bool IsPaused() const {
+        std::unique_lock lk(suspend_guard);
+        return is_paused;
+    }
+
     std::unique_lock<std::mutex> StallProcesses() {
         std::unique_lock<std::mutex> lk(suspend_guard);
         kernel.Suspend(true);
@@ -214,6 +224,8 @@ struct System::Impl {
             return SystemResultStatus::ErrorVideoCore;
         }
 
+        audio_core = std::make_unique<AudioCore::AudioCore>(system);
+
         service_manager = std::make_shared<Service::SM::ServiceManager>(kernel);
         services = std::make_unique<Service::Services>(service_manager, system);
         interrupt_manager = std::make_unique<Hardware::InterruptManager>(system);
@@ -290,7 +302,7 @@ struct System::Impl {
             if (Settings::values.gamecard_current_game) {
                 fs_controller.SetGameCard(GetGameFileFromPath(virtual_filesystem, filepath));
             } else if (!Settings::values.gamecard_path.GetValue().empty()) {
-                const auto gamecard_path = Settings::values.gamecard_path.GetValue();
+                const auto& gamecard_path = Settings::values.gamecard_path.GetValue();
                 fs_controller.SetGameCard(GetGameFileFromPath(virtual_filesystem, gamecard_path));
             }
         }
@@ -308,6 +320,8 @@ struct System::Impl {
     }
 
     void Shutdown() {
+        SetShuttingDown(true);
+
         // Log last frame performance stats if game was loded
         if (perf_stats) {
             const auto perf_results = GetAndResetPerfStats();
@@ -333,14 +347,15 @@ struct System::Impl {
         kernel.ShutdownCores();
         cpu_manager.Shutdown();
         debugger.reset();
+        kernel.CloseServices();
         services.reset();
         service_manager.reset();
         cheat_engine.reset();
         telemetry_session.reset();
-        cpu_manager.Shutdown();
         time_manager.Shutdown();
         core_timing.Shutdown();
         app_loader.reset();
+        audio_core.reset();
         gpu_core.reset();
         perf_stats.reset();
         kernel.Shutdown();
@@ -350,6 +365,14 @@ struct System::Impl {
         LOG_DEBUG(Core, "Shutdown OK");
     }
 
+    bool IsShuttingDown() const {
+        return is_shutting_down;
+    }
+
+    void SetShuttingDown(bool shutting_down) {
+        is_shutting_down = shutting_down;
+    }
+
     Loader::ResultStatus GetGameName(std::string& out) const {
         if (app_loader == nullptr)
             return Loader::ResultStatus::ErrorNotInitialized;
@@ -392,8 +415,9 @@ struct System::Impl {
         return perf_stats->GetAndResetStats(core_timing.GetGlobalTimeUs());
     }
 
-    std::mutex suspend_guard;
+    mutable std::mutex suspend_guard;
     bool is_paused{};
+    std::atomic<bool> is_shutting_down{};
 
     Timing::CoreTiming core_timing;
     Kernel::KernelCore kernel;
@@ -407,6 +431,7 @@ struct System::Impl {
     std::unique_ptr<Tegra::GPU> gpu_core;
     std::unique_ptr<Hardware::InterruptManager> interrupt_manager;
     std::unique_ptr<Core::DeviceMemory> device_memory;
+    std::unique_ptr<AudioCore::AudioCore> audio_core;
     Core::Memory::Memory memory;
     Core::HID::HIDCore hid_core;
     CpuManager cpu_manager;
@@ -479,6 +504,10 @@ SystemResultStatus System::Pause() {
     return impl->Pause();
 }
 
+bool System::IsPaused() const {
+    return impl->IsPaused();
+}
+
 void System::InvalidateCpuInstructionCaches() {
     impl->kernel.InvalidateAllInstructionCaches();
 }
@@ -491,6 +520,14 @@ void System::Shutdown() {
     impl->Shutdown();
 }
 
+bool System::IsShuttingDown() const {
+    return impl->IsShuttingDown();
+}
+
+void System::SetShuttingDown(bool shutting_down) {
+    impl->SetShuttingDown(shutting_down);
+}
+
 void System::DetachDebugger() {
     if (impl->debugger) {
         impl->debugger->NotifyShutdown();
@@ -640,6 +677,14 @@ const HID::HIDCore& System::HIDCore() const {
     return impl->hid_core;
 }
 
+AudioCore::AudioCore& System::AudioCore() {
+    return *impl->audio_core;
+}
+
+const AudioCore::AudioCore& System::AudioCore() const {
+    return *impl->audio_core;
+}
+
 Timing::CoreTiming& System::CoreTiming() {
     return impl->core_timing;
 }
diff --git a/src/core/core.h b/src/core/core.h
index 60efe44103..a49d1214b7 100644
--- a/src/core/core.h
+++ b/src/core/core.h
@@ -81,6 +81,10 @@ namespace VideoCore {
 class RendererBase;
 } // namespace VideoCore
 
+namespace AudioCore {
+class AudioCore;
+} // namespace AudioCore
+
 namespace Core::Timing {
 class CoreTiming;
 }
@@ -148,6 +152,9 @@ public:
      */
     [[nodiscard]] SystemResultStatus Pause();
 
+    /// Check if the core is currently paused.
+    [[nodiscard]] bool IsPaused() const;
+
     /**
      * Invalidate the CPU instruction caches
      * This function should only be used by GDB Stub to support breakpoints, memory updates and
@@ -160,6 +167,12 @@ public:
     /// Shutdown the emulated system.
     void Shutdown();
 
+    /// Check if the core is shutting down.
+    [[nodiscard]] bool IsShuttingDown() const;
+
+    /// Set the shutting down state.
+    void SetShuttingDown(bool shutting_down);
+
     /// Forcibly detach the debugger if it is running.
     void DetachDebugger();
 
@@ -250,6 +263,12 @@ public:
     /// Gets an immutable reference to the renderer.
     [[nodiscard]] const VideoCore::RendererBase& Renderer() const;
 
+    /// Gets a mutable reference to the audio interface
+    [[nodiscard]] AudioCore::AudioCore& AudioCore();
+
+    /// Gets an immutable reference to the audio interface.
+    [[nodiscard]] const AudioCore::AudioCore& AudioCore() const;
+
     /// Gets the global scheduler
     [[nodiscard]] Kernel::GlobalSchedulerContext& GlobalSchedulerContext();
 
diff --git a/src/core/hle/kernel/hle_ipc.cpp b/src/core/hle/kernel/hle_ipc.cpp
index 45135a07ff..5b3feec662 100644
--- a/src/core/hle/kernel/hle_ipc.cpp
+++ b/src/core/hle/kernel/hle_ipc.cpp
@@ -287,18 +287,52 @@ std::size_t HLERequestContext::WriteBuffer(const void* buffer, std::size_t size,
             BufferDescriptorB().size() > buffer_index &&
                 BufferDescriptorB()[buffer_index].Size() >= size,
             { return 0; }, "BufferDescriptorB is invalid, index={}, size={}", buffer_index, size);
-        memory.WriteBlock(BufferDescriptorB()[buffer_index].Address(), buffer, size);
+        WriteBufferB(buffer, size, buffer_index);
     } else {
         ASSERT_OR_EXECUTE_MSG(
             BufferDescriptorC().size() > buffer_index &&
                 BufferDescriptorC()[buffer_index].Size() >= size,
             { return 0; }, "BufferDescriptorC is invalid, index={}, size={}", buffer_index, size);
-        memory.WriteBlock(BufferDescriptorC()[buffer_index].Address(), buffer, size);
+        WriteBufferC(buffer, size, buffer_index);
     }
 
     return size;
 }
 
+std::size_t HLERequestContext::WriteBufferB(const void* buffer, std::size_t size,
+                                            std::size_t buffer_index) const {
+    if (buffer_index >= BufferDescriptorB().size() || size == 0) {
+        return 0;
+    }
+
+    const auto buffer_size{BufferDescriptorB()[buffer_index].Size()};
+    if (size > buffer_size) {
+        LOG_CRITICAL(Core, "size ({:016X}) is greater than buffer_size ({:016X})", size,
+                     buffer_size);
+        size = buffer_size; // TODO(bunnei): This needs to be HW tested
+    }
+
+    memory.WriteBlock(BufferDescriptorB()[buffer_index].Address(), buffer, size);
+    return size;
+}
+
+std::size_t HLERequestContext::WriteBufferC(const void* buffer, std::size_t size,
+                                            std::size_t buffer_index) const {
+    if (buffer_index >= BufferDescriptorC().size() || size == 0) {
+        return 0;
+    }
+
+    const auto buffer_size{BufferDescriptorC()[buffer_index].Size()};
+    if (size > buffer_size) {
+        LOG_CRITICAL(Core, "size ({:016X}) is greater than buffer_size ({:016X})", size,
+                     buffer_size);
+        size = buffer_size; // TODO(bunnei): This needs to be HW tested
+    }
+
+    memory.WriteBlock(BufferDescriptorC()[buffer_index].Address(), buffer, size);
+    return size;
+}
+
 std::size_t HLERequestContext::GetReadBufferSize(std::size_t buffer_index) const {
     const bool is_buffer_a{BufferDescriptorA().size() > buffer_index &&
                            BufferDescriptorA()[buffer_index].Size()};
diff --git a/src/core/hle/kernel/hle_ipc.h b/src/core/hle/kernel/hle_ipc.h
index d3abeee85b..99265ce90a 100644
--- a/src/core/hle/kernel/hle_ipc.h
+++ b/src/core/hle/kernel/hle_ipc.h
@@ -277,6 +277,14 @@ public:
     std::size_t WriteBuffer(const void* buffer, std::size_t size,
                             std::size_t buffer_index = 0) const;
 
+    /// Helper function to write buffer B
+    std::size_t WriteBufferB(const void* buffer, std::size_t size,
+                             std::size_t buffer_index = 0) const;
+
+    /// Helper function to write buffer C
+    std::size_t WriteBufferC(const void* buffer, std::size_t size,
+                             std::size_t buffer_index = 0) const;
+
     /* Helper function to write a buffer using the appropriate buffer descriptor
      *
      * @tparam T an arbitrary container that satisfies the
diff --git a/src/core/hle/kernel/kernel.cpp b/src/core/hle/kernel/kernel.cpp
index 7307cf262b..f23c629dc3 100644
--- a/src/core/hle/kernel/kernel.cpp
+++ b/src/core/hle/kernel/kernel.cpp
@@ -95,19 +95,7 @@ struct KernelCore::Impl {
 
         process_list.clear();
 
-        // Close all open server sessions and ports.
-        std::unordered_set<KAutoObject*> server_objects_;
-        {
-            std::scoped_lock lk(server_objects_lock);
-            server_objects_ = server_objects;
-            server_objects.clear();
-        }
-        for (auto* server_object : server_objects_) {
-            server_object->Close();
-        }
-
-        // Ensures all service threads gracefully shutdown.
-        ClearServiceThreads();
+        CloseServices();
 
         next_object_id = 0;
         next_kernel_process_id = KProcess::InitialKIPIDMin;
@@ -191,6 +179,22 @@ struct KernelCore::Impl {
         global_object_list_container.reset();
     }
 
+    void CloseServices() {
+        // Close all open server sessions and ports.
+        std::unordered_set<KAutoObject*> server_objects_;
+        {
+            std::scoped_lock lk(server_objects_lock);
+            server_objects_ = server_objects;
+            server_objects.clear();
+        }
+        for (auto* server_object : server_objects_) {
+            server_object->Close();
+        }
+
+        // Ensures all service threads gracefully shutdown.
+        ClearServiceThreads();
+    }
+
     void InitializePhysicalCores() {
         exclusive_monitor =
             Core::MakeExclusiveMonitor(system.Memory(), Core::Hardware::NUM_CPU_CORES);
@@ -813,6 +817,10 @@ void KernelCore::Shutdown() {
     impl->Shutdown();
 }
 
+void KernelCore::CloseServices() {
+    impl->CloseServices();
+}
+
 const KResourceLimit* KernelCore::GetSystemResourceLimit() const {
     return impl->system_resource_limit;
 }
diff --git a/src/core/hle/kernel/kernel.h b/src/core/hle/kernel/kernel.h
index aa0ebaa02c..6c7cf6af2e 100644
--- a/src/core/hle/kernel/kernel.h
+++ b/src/core/hle/kernel/kernel.h
@@ -109,6 +109,9 @@ public:
     /// Clears all resources in use by the kernel instance.
     void Shutdown();
 
+    /// Close all active services in use by the kernel instance.
+    void CloseServices();
+
     /// Retrieves a shared pointer to the system resource limit instance.
     const KResourceLimit* GetSystemResourceLimit() const;
 
diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp
index 9116dd77c9..118f226e41 100644
--- a/src/core/hle/service/am/am.cpp
+++ b/src/core/hle/service/am/am.cpp
@@ -5,7 +5,6 @@
 #include <array>
 #include <cinttypes>
 #include <cstring>
-#include "audio_core/audio_renderer.h"
 #include "common/settings.h"
 #include "core/core.h"
 #include "core/file_sys/control_metadata.h"
@@ -286,7 +285,7 @@ ISelfController::ISelfController(Core::System& system_, NVFlinger::NVFlinger& nv
         {62, &ISelfController::SetIdleTimeDetectionExtension, "SetIdleTimeDetectionExtension"},
         {63, &ISelfController::GetIdleTimeDetectionExtension, "GetIdleTimeDetectionExtension"},
         {64, nullptr, "SetInputDetectionSourceSet"},
-        {65, nullptr, "ReportUserIsActive"},
+        {65, &ISelfController::ReportUserIsActive, "ReportUserIsActive"},
         {66, nullptr, "GetCurrentIlluminance"},
         {67, nullptr, "IsIlluminanceAvailable"},
         {68, &ISelfController::SetAutoSleepDisabled, "SetAutoSleepDisabled"},
@@ -518,6 +517,13 @@ void ISelfController::GetIdleTimeDetectionExtension(Kernel::HLERequestContext& c
     rb.Push<u32>(idle_time_detection_extension);
 }
 
+void ISelfController::ReportUserIsActive(Kernel::HLERequestContext& ctx) {
+    LOG_WARNING(Service_AM, "(STUBBED) called");
+
+    IPC::ResponseBuilder rb{ctx, 2};
+    rb.Push(ResultSuccess);
+}
+
 void ISelfController::SetAutoSleepDisabled(Kernel::HLERequestContext& ctx) {
     IPC::RequestParser rp{ctx};
     is_auto_sleep_disabled = rp.Pop<bool>();
diff --git a/src/core/hle/service/am/am.h b/src/core/hle/service/am/am.h
index 53144427b4..bb75c62817 100644
--- a/src/core/hle/service/am/am.h
+++ b/src/core/hle/service/am/am.h
@@ -175,6 +175,7 @@ private:
     void SetHandlesRequestToDisplay(Kernel::HLERequestContext& ctx);
     void SetIdleTimeDetectionExtension(Kernel::HLERequestContext& ctx);
     void GetIdleTimeDetectionExtension(Kernel::HLERequestContext& ctx);
+    void ReportUserIsActive(Kernel::HLERequestContext& ctx);
     void SetAutoSleepDisabled(Kernel::HLERequestContext& ctx);
     void IsAutoSleepDisabled(Kernel::HLERequestContext& ctx);
     void GetAccumulatedSuspendedTickValue(Kernel::HLERequestContext& ctx);
diff --git a/src/core/hle/service/audio/audin_u.cpp b/src/core/hle/service/audio/audin_u.cpp
index 18d3ae6829..48a9a73a0e 100644
--- a/src/core/hle/service/audio/audin_u.cpp
+++ b/src/core/hle/service/audio/audin_u.cpp
@@ -1,68 +1,211 @@
 // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
 // SPDX-License-Identifier: GPL-2.0-or-later
 
+#include "audio_core/in/audio_in_system.h"
+#include "audio_core/renderer/audio_device.h"
+#include "common/common_funcs.h"
 #include "common/logging/log.h"
+#include "common/string_util.h"
 #include "core/core.h"
 #include "core/hle/ipc_helpers.h"
 #include "core/hle/kernel/k_event.h"
 #include "core/hle/service/audio/audin_u.h"
 
 namespace Service::Audio {
+using namespace AudioCore::AudioIn;
 
-IAudioIn::IAudioIn(Core::System& system_)
-    : ServiceFramework{system_, "IAudioIn"}, service_context{system_, "IAudioIn"} {
-    // clang-format off
-    static const FunctionInfo functions[] = {
-        {0, nullptr, "GetAudioInState"},
-        {1, &IAudioIn::Start, "Start"},
-        {2, nullptr, "Stop"},
-        {3, nullptr, "AppendAudioInBuffer"},
-        {4, &IAudioIn::RegisterBufferEvent, "RegisterBufferEvent"},
-        {5, nullptr, "GetReleasedAudioInBuffer"},
-        {6, nullptr, "ContainsAudioInBuffer"},
-        {7, nullptr, "AppendUacInBuffer"},
-        {8, &IAudioIn::AppendAudioInBufferAuto, "AppendAudioInBufferAuto"},
-        {9, nullptr, "GetReleasedAudioInBuffersAuto"},
-        {10, nullptr, "AppendUacInBufferAuto"},
-        {11, nullptr, "GetAudioInBufferCount"},
-        {12, nullptr, "SetDeviceGain"},
-        {13, nullptr, "GetDeviceGain"},
-        {14, nullptr, "FlushAudioInBuffers"},
-    };
-    // clang-format on
+class IAudioIn final : public ServiceFramework<IAudioIn> {
+public:
+    explicit IAudioIn(Core::System& system_, Manager& manager, size_t session_id,
+                      std::string& device_name, const AudioInParameter& in_params, u32 handle,
+                      u64 applet_resource_user_id)
+        : ServiceFramework{system_, "IAudioIn"},
+          service_context{system_, "IAudioIn"}, event{service_context.CreateEvent("AudioInEvent")},
+          impl{std::make_shared<In>(system_, manager, event, session_id)} {
+        // clang-format off
+        static const FunctionInfo functions[] = {
+            {0, &IAudioIn::GetAudioInState, "GetAudioInState"},
+            {1, &IAudioIn::Start, "Start"},
+            {2, &IAudioIn::Stop, "Stop"},
+            {3, &IAudioIn::AppendAudioInBuffer, "AppendAudioInBuffer"},
+            {4, &IAudioIn::RegisterBufferEvent, "RegisterBufferEvent"},
+            {5, &IAudioIn::GetReleasedAudioInBuffer, "GetReleasedAudioInBuffer"},
+            {6, &IAudioIn::ContainsAudioInBuffer, "ContainsAudioInBuffer"},
+            {7, &IAudioIn::AppendAudioInBuffer, "AppendUacInBuffer"},
+            {8, &IAudioIn::AppendAudioInBuffer, "AppendAudioInBufferAuto"},
+            {9, &IAudioIn::GetReleasedAudioInBuffer, "GetReleasedAudioInBuffersAuto"},
+            {10, &IAudioIn::AppendAudioInBuffer, "AppendUacInBufferAuto"},
+            {11, &IAudioIn::GetAudioInBufferCount, "GetAudioInBufferCount"},
+            {12, &IAudioIn::SetDeviceGain, "SetDeviceGain"},
+            {13, &IAudioIn::GetDeviceGain, "GetDeviceGain"},
+            {14, &IAudioIn::FlushAudioInBuffers, "FlushAudioInBuffers"},
+        };
+        // clang-format on
 
-    RegisterHandlers(functions);
+        RegisterHandlers(functions);
 
-    buffer_event = service_context.CreateEvent("IAudioIn:BufferEvent");
-}
+        if (impl->GetSystem()
+                .Initialize(device_name, in_params, handle, applet_resource_user_id)
+                .IsError()) {
+            LOG_ERROR(Service_Audio, "Failed to initialize the AudioIn System!");
+        }
+    }
 
-IAudioIn::~IAudioIn() {
-    service_context.CloseEvent(buffer_event);
-}
+    ~IAudioIn() override {
+        impl->Free();
+        service_context.CloseEvent(event);
+    }
 
-void IAudioIn::Start(Kernel::HLERequestContext& ctx) {
-    LOG_WARNING(Service_Audio, "(STUBBED) called");
+    [[nodiscard]] std::shared_ptr<In> GetImpl() {
+        return impl;
+    }
 
-    IPC::ResponseBuilder rb{ctx, 2};
-    rb.Push(ResultSuccess);
-}
+private:
+    void GetAudioInState(Kernel::HLERequestContext& ctx) {
+        const auto state = static_cast<u32>(impl->GetState());
 
-void IAudioIn::RegisterBufferEvent(Kernel::HLERequestContext& ctx) {
-    LOG_WARNING(Service_Audio, "(STUBBED) called");
+        LOG_DEBUG(Service_Audio, "called. State={}", state);
 
-    IPC::ResponseBuilder rb{ctx, 2, 1};
-    rb.Push(ResultSuccess);
-    rb.PushCopyObjects(buffer_event->GetReadableEvent());
-}
+        IPC::ResponseBuilder rb{ctx, 3};
+        rb.Push(ResultSuccess);
+        rb.Push(state);
+    }
 
-void IAudioIn::AppendAudioInBufferAuto(Kernel::HLERequestContext& ctx) {
-    LOG_WARNING(Service_Audio, "(STUBBED) called");
+    void Start(Kernel::HLERequestContext& ctx) {
+        LOG_DEBUG(Service_Audio, "called");
 
-    IPC::ResponseBuilder rb{ctx, 2};
-    rb.Push(ResultSuccess);
-}
+        auto result = impl->StartSystem();
 
-AudInU::AudInU(Core::System& system_) : ServiceFramework{system_, "audin:u"} {
+        IPC::ResponseBuilder rb{ctx, 2};
+        rb.Push(result);
+    }
+
+    void Stop(Kernel::HLERequestContext& ctx) {
+        LOG_DEBUG(Service_Audio, "called");
+
+        auto result = impl->StopSystem();
+
+        IPC::ResponseBuilder rb{ctx, 2};
+        rb.Push(result);
+    }
+
+    void AppendAudioInBuffer(Kernel::HLERequestContext& ctx) {
+        IPC::RequestParser rp{ctx};
+        u64 tag = rp.PopRaw<u64>();
+
+        const auto in_buffer_size{ctx.GetReadBufferSize()};
+        if (in_buffer_size < sizeof(AudioInBuffer)) {
+            LOG_ERROR(Service_Audio, "Input buffer is too small for an AudioInBuffer!");
+        }
+
+        const auto& in_buffer = ctx.ReadBuffer();
+        AudioInBuffer buffer{};
+        std::memcpy(&buffer, in_buffer.data(), sizeof(AudioInBuffer));
+
+        [[maybe_unused]] auto sessionid{impl->GetSystem().GetSessionId()};
+        LOG_TRACE(Service_Audio, "called. Session {} Appending buffer {:08X}", sessionid, tag);
+
+        auto result = impl->AppendBuffer(buffer, tag);
+
+        IPC::ResponseBuilder rb{ctx, 2};
+        rb.Push(result);
+    }
+
+    void RegisterBufferEvent(Kernel::HLERequestContext& ctx) {
+        LOG_DEBUG(Service_Audio, "called");
+
+        auto& buffer_event = impl->GetBufferEvent();
+
+        IPC::ResponseBuilder rb{ctx, 2, 1};
+        rb.Push(ResultSuccess);
+        rb.PushCopyObjects(buffer_event);
+    }
+
+    void GetReleasedAudioInBuffer(Kernel::HLERequestContext& ctx) {
+        auto write_buffer_size = ctx.GetWriteBufferSize() / sizeof(u64);
+        std::vector<u64> released_buffers(write_buffer_size, 0);
+
+        auto count = impl->GetReleasedBuffers(released_buffers);
+
+        [[maybe_unused]] std::string tags{};
+        for (u32 i = 0; i < count; i++) {
+            tags += fmt::format("{:08X}, ", released_buffers[i]);
+        }
+        [[maybe_unused]] auto sessionid{impl->GetSystem().GetSessionId()};
+        LOG_TRACE(Service_Audio, "called. Session {} released {} buffers: {}", sessionid, count,
+                  tags);
+
+        ctx.WriteBuffer(released_buffers);
+        IPC::ResponseBuilder rb{ctx, 3};
+        rb.Push(ResultSuccess);
+        rb.Push(count);
+    }
+
+    void ContainsAudioInBuffer(Kernel::HLERequestContext& ctx) {
+        IPC::RequestParser rp{ctx};
+
+        const u64 tag{rp.Pop<u64>()};
+        const auto buffer_queued{impl->ContainsAudioBuffer(tag)};
+
+        LOG_DEBUG(Service_Audio, "called. Is buffer {:08X} registered? {}", tag, buffer_queued);
+
+        IPC::ResponseBuilder rb{ctx, 3};
+        rb.Push(ResultSuccess);
+        rb.Push(buffer_queued);
+    }
+
+    void GetAudioInBufferCount(Kernel::HLERequestContext& ctx) {
+        const auto buffer_count = impl->GetBufferCount();
+
+        LOG_DEBUG(Service_Audio, "called. Buffer count={}", buffer_count);
+
+        IPC::ResponseBuilder rb{ctx, 3};
+
+        rb.Push(ResultSuccess);
+        rb.Push(buffer_count);
+    }
+
+    void SetDeviceGain(Kernel::HLERequestContext& ctx) {
+        IPC::RequestParser rp{ctx};
+
+        const auto volume{rp.Pop<f32>()};
+        LOG_DEBUG(Service_Audio, "called. Gain {}", volume);
+
+        impl->SetVolume(volume);
+
+        IPC::ResponseBuilder rb{ctx, 2};
+        rb.Push(ResultSuccess);
+    }
+
+    void GetDeviceGain(Kernel::HLERequestContext& ctx) {
+        auto volume{impl->GetVolume()};
+
+        LOG_DEBUG(Service_Audio, "called. Gain {}", volume);
+
+        IPC::ResponseBuilder rb{ctx, 3};
+        rb.Push(ResultSuccess);
+        rb.Push(volume);
+    }
+
+    void FlushAudioInBuffers(Kernel::HLERequestContext& ctx) {
+        bool flushed{impl->FlushAudioInBuffers()};
+
+        LOG_DEBUG(Service_Audio, "called. Were any buffers flushed? {}", flushed);
+
+        IPC::ResponseBuilder rb{ctx, 3};
+        rb.Push(ResultSuccess);
+        rb.Push(flushed);
+    }
+
+    KernelHelpers::ServiceContext service_context;
+    Kernel::KEvent* event;
+    std::shared_ptr<AudioCore::AudioIn::In> impl;
+};
+
+AudInU::AudInU(Core::System& system_)
+    : ServiceFramework{system_, "audin:u", ServiceThreadType::CreateNew},
+      service_context{system_, "AudInU"}, impl{std::make_unique<AudioCore::AudioIn::Manager>(
+                                              system_)} {
     // clang-format off
     static const FunctionInfo functions[] = {
         {0, &AudInU::ListAudioIns, "ListAudioIns"},
@@ -80,59 +223,152 @@ AudInU::AudInU(Core::System& system_) : ServiceFramework{system_, "audin:u"} {
 AudInU::~AudInU() = default;
 
 void AudInU::ListAudioIns(Kernel::HLERequestContext& ctx) {
+    using namespace AudioCore::AudioRenderer;
+
     LOG_DEBUG(Service_Audio, "called");
-    const std::size_t count = ctx.GetWriteBufferSize() / sizeof(AudioInDeviceName);
 
-    const std::size_t device_count = std::min(count, audio_device_names.size());
-    std::vector<AudioInDeviceName> device_names;
-    device_names.reserve(device_count);
+    const auto write_count =
+        static_cast<u32>(ctx.GetWriteBufferSize() / sizeof(AudioDevice::AudioDeviceName));
+    std::vector<AudioDevice::AudioDeviceName> device_names{};
 
-    for (std::size_t i = 0; i < device_count; i++) {
-        const auto& device_name = audio_device_names[i];
-        auto& entry = device_names.emplace_back();
-        device_name.copy(entry.data(), device_name.size());
+    u32 out_count{0};
+    if (write_count > 0) {
+        out_count = impl->GetDeviceNames(device_names, write_count, false);
+        ctx.WriteBuffer(device_names);
     }
 
-    ctx.WriteBuffer(device_names);
-
     IPC::ResponseBuilder rb{ctx, 3};
     rb.Push(ResultSuccess);
-    rb.Push(static_cast<u32>(device_names.size()));
+    rb.Push(out_count);
 }
 
 void AudInU::ListAudioInsAutoFiltered(Kernel::HLERequestContext& ctx) {
-    LOG_DEBUG(Service_Audio, "called");
-    constexpr u32 device_count = 0;
+    using namespace AudioCore::AudioRenderer;
 
-    // Since we don't actually use any other audio input devices, we return 0 devices. Filtered
-    // device listing just omits the default input device
+    LOG_DEBUG(Service_Audio, "called");
+
+    const auto write_count =
+        static_cast<u32>(ctx.GetWriteBufferSize() / sizeof(AudioDevice::AudioDeviceName));
+    std::vector<AudioDevice::AudioDeviceName> device_names{};
+
+    u32 out_count{0};
+    if (write_count > 0) {
+        out_count = impl->GetDeviceNames(device_names, write_count, true);
+        ctx.WriteBuffer(device_names);
+    }
 
     IPC::ResponseBuilder rb{ctx, 3};
     rb.Push(ResultSuccess);
-    rb.Push(static_cast<u32>(device_count));
-}
-
-void AudInU::OpenInOutImpl(Kernel::HLERequestContext& ctx) {
-    AudInOutParams params{};
-    params.channel_count = 2;
-    params.sample_format = SampleFormat::PCM16;
-    params.sample_rate = 48000;
-    params.state = State::Started;
-
-    IPC::ResponseBuilder rb{ctx, 6, 0, 1};
-    rb.Push(ResultSuccess);
-    rb.PushRaw<AudInOutParams>(params);
-    rb.PushIpcInterface<IAudioIn>(system);
+    rb.Push(out_count);
 }
 
 void AudInU::OpenAudioIn(Kernel::HLERequestContext& ctx) {
-    LOG_WARNING(Service_Audio, "(STUBBED) called");
-    OpenInOutImpl(ctx);
+    IPC::RequestParser rp{ctx};
+    auto in_params{rp.PopRaw<AudioInParameter>()};
+    auto applet_resource_user_id{rp.PopRaw<u64>()};
+    const auto device_name_data{ctx.ReadBuffer()};
+    auto device_name = Common::StringFromBuffer(device_name_data);
+    auto handle{ctx.GetCopyHandle(0)};
+
+    std::scoped_lock l{impl->mutex};
+    auto link{impl->LinkToManager()};
+    if (link.IsError()) {
+        LOG_ERROR(Service_Audio, "Failed to link Audio In to Audio Manager");
+        IPC::ResponseBuilder rb{ctx, 2};
+        rb.Push(link);
+        return;
+    }
+
+    size_t new_session_id{};
+    auto result{impl->AcquireSessionId(new_session_id)};
+    if (result.IsError()) {
+        IPC::ResponseBuilder rb{ctx, 2};
+        rb.Push(result);
+        return;
+    }
+
+    LOG_DEBUG(Service_Audio, "Opening new AudioIn, sessionid={}, free sessions={}", new_session_id,
+              impl->num_free_sessions);
+
+    auto audio_in = std::make_shared<IAudioIn>(system, *impl, new_session_id, device_name,
+                                               in_params, handle, applet_resource_user_id);
+    impl->sessions[new_session_id] = audio_in->GetImpl();
+    impl->applet_resource_user_ids[new_session_id] = applet_resource_user_id;
+
+    auto& out_system = impl->sessions[new_session_id]->GetSystem();
+    AudioInParameterInternal out_params{.sample_rate = out_system.GetSampleRate(),
+                                        .channel_count = out_system.GetChannelCount(),
+                                        .sample_format =
+                                            static_cast<u32>(out_system.GetSampleFormat()),
+                                        .state = static_cast<u32>(out_system.GetState())};
+
+    IPC::ResponseBuilder rb{ctx, 6, 0, 1};
+
+    std::string out_name{out_system.GetName()};
+    ctx.WriteBuffer(out_name);
+
+    rb.Push(ResultSuccess);
+    rb.PushRaw<AudioInParameterInternal>(out_params);
+    rb.PushIpcInterface<IAudioIn>(audio_in);
 }
 
 void AudInU::OpenAudioInProtocolSpecified(Kernel::HLERequestContext& ctx) {
-    LOG_WARNING(Service_Audio, "(STUBBED) called");
-    OpenInOutImpl(ctx);
+    IPC::RequestParser rp{ctx};
+    auto protocol_specified{rp.PopRaw<u64>()};
+    auto in_params{rp.PopRaw<AudioInParameter>()};
+    auto applet_resource_user_id{rp.PopRaw<u64>()};
+    const auto device_name_data{ctx.ReadBuffer()};
+    auto device_name = Common::StringFromBuffer(device_name_data);
+    auto handle{ctx.GetCopyHandle(0)};
+
+    std::scoped_lock l{impl->mutex};
+    auto link{impl->LinkToManager()};
+    if (link.IsError()) {
+        LOG_ERROR(Service_Audio, "Failed to link Audio In to Audio Manager");
+        IPC::ResponseBuilder rb{ctx, 2};
+        rb.Push(link);
+        return;
+    }
+
+    size_t new_session_id{};
+    auto result{impl->AcquireSessionId(new_session_id)};
+    if (result.IsError()) {
+        IPC::ResponseBuilder rb{ctx, 2};
+        rb.Push(result);
+        return;
+    }
+
+    LOG_DEBUG(Service_Audio, "Opening new AudioIn, sessionid={}, free sessions={}", new_session_id,
+              impl->num_free_sessions);
+
+    auto audio_in = std::make_shared<IAudioIn>(system, *impl, new_session_id, device_name,
+                                               in_params, handle, applet_resource_user_id);
+    impl->sessions[new_session_id] = audio_in->GetImpl();
+    impl->applet_resource_user_ids[new_session_id] = applet_resource_user_id;
+
+    auto& out_system = impl->sessions[new_session_id]->GetSystem();
+    AudioInParameterInternal out_params{.sample_rate = out_system.GetSampleRate(),
+                                        .channel_count = out_system.GetChannelCount(),
+                                        .sample_format =
+                                            static_cast<u32>(out_system.GetSampleFormat()),
+                                        .state = static_cast<u32>(out_system.GetState())};
+
+    IPC::ResponseBuilder rb{ctx, 6, 0, 1};
+
+    std::string out_name{out_system.GetName()};
+    if (protocol_specified == 0) {
+        if (out_system.IsUac()) {
+            out_name = "UacIn";
+        } else {
+            out_name = "DeviceIn";
+        }
+    }
+
+    ctx.WriteBuffer(out_name);
+
+    rb.Push(ResultSuccess);
+    rb.PushRaw<AudioInParameterInternal>(out_params);
+    rb.PushIpcInterface<IAudioIn>(audio_in);
 }
 
 } // namespace Service::Audio
diff --git a/src/core/hle/service/audio/audin_u.h b/src/core/hle/service/audio/audin_u.h
index 2bfa38eccb..b45fda78ac 100644
--- a/src/core/hle/service/audio/audin_u.h
+++ b/src/core/hle/service/audio/audin_u.h
@@ -3,6 +3,8 @@
 
 #pragma once
 
+#include "audio_core/audio_in_manager.h"
+#include "audio_core/in/audio_in.h"
 #include "core/hle/service/kernel_helpers.h"
 #include "core/hle/service/service.h"
 
@@ -14,56 +16,27 @@ namespace Kernel {
 class HLERequestContext;
 }
 
+namespace AudioCore::AudioOut {
+class Manager;
+class In;
+} // namespace AudioCore::AudioOut
+
 namespace Service::Audio {
 
-class IAudioIn final : public ServiceFramework<IAudioIn> {
-public:
-    explicit IAudioIn(Core::System& system_);
-    ~IAudioIn() override;
-
-private:
-    void Start(Kernel::HLERequestContext& ctx);
-    void RegisterBufferEvent(Kernel::HLERequestContext& ctx);
-    void AppendAudioInBufferAuto(Kernel::HLERequestContext& ctx);
-
-    KernelHelpers::ServiceContext service_context;
-
-    Kernel::KEvent* buffer_event;
-};
-
 class AudInU final : public ServiceFramework<AudInU> {
 public:
     explicit AudInU(Core::System& system_);
     ~AudInU() override;
 
 private:
-    enum class SampleFormat : u32_le {
-        PCM16 = 2,
-    };
-
-    enum class State : u32_le {
-        Started = 0,
-        Stopped = 1,
-    };
-
-    struct AudInOutParams {
-        u32_le sample_rate{};
-        u32_le channel_count{};
-        SampleFormat sample_format{};
-        State state{};
-    };
-    static_assert(sizeof(AudInOutParams) == 0x10, "AudInOutParams is an invalid size");
-
-    using AudioInDeviceName = std::array<char, 256>;
-    static constexpr std::array<std::string_view, 1> audio_device_names{{
-        "BuiltInHeadset",
-    }};
-
     void ListAudioIns(Kernel::HLERequestContext& ctx);
     void ListAudioInsAutoFiltered(Kernel::HLERequestContext& ctx);
     void OpenInOutImpl(Kernel::HLERequestContext& ctx);
     void OpenAudioIn(Kernel::HLERequestContext& ctx);
     void OpenAudioInProtocolSpecified(Kernel::HLERequestContext& ctx);
+
+    KernelHelpers::ServiceContext service_context;
+    std::unique_ptr<AudioCore::AudioIn::Manager> impl;
 };
 
 } // namespace Service::Audio
diff --git a/src/core/hle/service/audio/audout_u.cpp b/src/core/hle/service/audio/audout_u.cpp
index b0dad60535..a44dd842a0 100644
--- a/src/core/hle/service/audio/audout_u.cpp
+++ b/src/core/hle/service/audio/audout_u.cpp
@@ -5,56 +5,43 @@
 #include <cstring>
 #include <vector>
 
-#include "audio_core/audio_out.h"
-#include "audio_core/codec.h"
+#include "audio_core/out/audio_out_system.h"
+#include "audio_core/renderer/audio_device.h"
 #include "common/common_funcs.h"
 #include "common/logging/log.h"
+#include "common/string_util.h"
 #include "common/swap.h"
 #include "core/core.h"
 #include "core/hle/ipc_helpers.h"
 #include "core/hle/kernel/k_event.h"
 #include "core/hle/service/audio/audout_u.h"
 #include "core/hle/service/audio/errors.h"
-#include "core/hle/service/kernel_helpers.h"
 #include "core/memory.h"
 
 namespace Service::Audio {
-
-constexpr std::array<char, 10> DefaultDevice{{"DeviceOut"}};
-constexpr int DefaultSampleRate{48000};
-
-struct AudoutParams {
-    s32_le sample_rate;
-    u16_le channel_count;
-    INSERT_PADDING_BYTES_NOINIT(2);
-};
-static_assert(sizeof(AudoutParams) == 0x8, "AudoutParams is an invalid size");
-
-enum class AudioState : u32 {
-    Started,
-    Stopped,
-};
+using namespace AudioCore::AudioOut;
 
 class IAudioOut final : public ServiceFramework<IAudioOut> {
 public:
-    explicit IAudioOut(Core::System& system_, AudoutParams audio_params_,
-                       AudioCore::AudioOut& audio_core_, std::string&& device_name_,
-                       std::string&& unique_name)
+    explicit IAudioOut(Core::System& system_, AudioCore::AudioOut::Manager& manager,
+                       size_t session_id, std::string& device_name,
+                       const AudioOutParameter& in_params, u32 handle, u64 applet_resource_user_id)
         : ServiceFramework{system_, "IAudioOut", ServiceThreadType::CreateNew},
-          audio_core{audio_core_}, device_name{std::move(device_name_)},
-          audio_params{audio_params_}, main_memory{system.Memory()}, service_context{system_,
-                                                                                     "IAudioOut"} {
+          service_context{system_, "IAudioOut"}, event{service_context.CreateEvent(
+                                                     "AudioOutEvent")},
+          impl{std::make_shared<AudioCore::AudioOut::Out>(system_, manager, event, session_id)} {
+
         // clang-format off
         static const FunctionInfo functions[] = {
             {0, &IAudioOut::GetAudioOutState, "GetAudioOutState"},
-            {1, &IAudioOut::StartAudioOut, "Start"},
-            {2, &IAudioOut::StopAudioOut, "Stop"},
-            {3, &IAudioOut::AppendAudioOutBufferImpl, "AppendAudioOutBuffer"},
+            {1, &IAudioOut::Start, "Start"},
+            {2, &IAudioOut::Stop, "Stop"},
+            {3, &IAudioOut::AppendAudioOutBuffer, "AppendAudioOutBuffer"},
             {4, &IAudioOut::RegisterBufferEvent, "RegisterBufferEvent"},
-            {5, &IAudioOut::GetReleasedAudioOutBufferImpl, "GetReleasedAudioOutBuffers"},
+            {5, &IAudioOut::GetReleasedAudioOutBuffers, "GetReleasedAudioOutBuffers"},
             {6, &IAudioOut::ContainsAudioOutBuffer, "ContainsAudioOutBuffer"},
-            {7, &IAudioOut::AppendAudioOutBufferImpl, "AppendAudioOutBufferAuto"},
-            {8, &IAudioOut::GetReleasedAudioOutBufferImpl, "GetReleasedAudioOutBufferAuto"},
+            {7, &IAudioOut::AppendAudioOutBuffer, "AppendAudioOutBufferAuto"},
+            {8, &IAudioOut::GetReleasedAudioOutBuffers, "GetReleasedAudioOutBuffersAuto"},
             {9, &IAudioOut::GetAudioOutBufferCount, "GetAudioOutBufferCount"},
             {10, &IAudioOut::GetAudioOutPlayedSampleCount, "GetAudioOutPlayedSampleCount"},
             {11, &IAudioOut::FlushAudioOutBuffers, "FlushAudioOutBuffers"},
@@ -64,241 +51,263 @@ public:
         // clang-format on
         RegisterHandlers(functions);
 
-        // This is the event handle used to check if the audio buffer was released
-        buffer_event = service_context.CreateEvent("IAudioOutBufferReleased");
-
-        stream = audio_core.OpenStream(system.CoreTiming(), audio_params.sample_rate,
-                                       audio_params.channel_count, std::move(unique_name), [this] {
-                                           const auto guard = LockService();
-                                           buffer_event->GetWritableEvent().Signal();
-                                       });
+        if (impl->GetSystem()
+                .Initialize(device_name, in_params, handle, applet_resource_user_id)
+                .IsError()) {
+            LOG_ERROR(Service_Audio, "Failed to initialize the AudioOut System!");
+        }
     }
 
     ~IAudioOut() override {
-        service_context.CloseEvent(buffer_event);
+        impl->Free();
+        service_context.CloseEvent(event);
+    }
+
+    [[nodiscard]] std::shared_ptr<AudioCore::AudioOut::Out> GetImpl() {
+        return impl;
     }
 
 private:
-    struct AudioBuffer {
-        u64_le next;
-        u64_le buffer;
-        u64_le buffer_capacity;
-        u64_le buffer_size;
-        u64_le offset;
-    };
-    static_assert(sizeof(AudioBuffer) == 0x28, "AudioBuffer is an invalid size");
-
     void GetAudioOutState(Kernel::HLERequestContext& ctx) {
-        LOG_DEBUG(Service_Audio, "called");
+        const auto state = static_cast<u32>(impl->GetState());
+
+        LOG_DEBUG(Service_Audio, "called. State={}", state);
 
         IPC::ResponseBuilder rb{ctx, 3};
         rb.Push(ResultSuccess);
-        rb.Push(static_cast<u32>(stream->IsPlaying() ? AudioState::Started : AudioState::Stopped));
+        rb.Push(state);
     }
 
-    void StartAudioOut(Kernel::HLERequestContext& ctx) {
+    void Start(Kernel::HLERequestContext& ctx) {
         LOG_DEBUG(Service_Audio, "called");
 
-        if (stream->IsPlaying()) {
-            IPC::ResponseBuilder rb{ctx, 2};
-            rb.Push(ERR_OPERATION_FAILED);
-            return;
-        }
-
-        audio_core.StartStream(stream);
+        auto result = impl->StartSystem();
 
         IPC::ResponseBuilder rb{ctx, 2};
-        rb.Push(ResultSuccess);
+        rb.Push(result);
     }
 
-    void StopAudioOut(Kernel::HLERequestContext& ctx) {
+    void Stop(Kernel::HLERequestContext& ctx) {
         LOG_DEBUG(Service_Audio, "called");
 
-        if (stream->IsPlaying()) {
-            audio_core.StopStream(stream);
-        }
+        auto result = impl->StopSystem();
 
         IPC::ResponseBuilder rb{ctx, 2};
-        rb.Push(ResultSuccess);
+        rb.Push(result);
+    }
+
+    void AppendAudioOutBuffer(Kernel::HLERequestContext& ctx) {
+        IPC::RequestParser rp{ctx};
+        u64 tag = rp.PopRaw<u64>();
+
+        const auto in_buffer_size{ctx.GetReadBufferSize()};
+        if (in_buffer_size < sizeof(AudioOutBuffer)) {
+            LOG_ERROR(Service_Audio, "Input buffer is too small for an AudioOutBuffer!");
+        }
+
+        const auto& in_buffer = ctx.ReadBuffer();
+        AudioOutBuffer buffer{};
+        std::memcpy(&buffer, in_buffer.data(), sizeof(AudioOutBuffer));
+
+        [[maybe_unused]] auto sessionid{impl->GetSystem().GetSessionId()};
+        LOG_TRACE(Service_Audio, "called. Session {} Appending buffer {:08X}", sessionid, tag);
+
+        auto result = impl->AppendBuffer(buffer, tag);
+
+        IPC::ResponseBuilder rb{ctx, 2};
+        rb.Push(result);
     }
 
     void RegisterBufferEvent(Kernel::HLERequestContext& ctx) {
         LOG_DEBUG(Service_Audio, "called");
 
+        auto& buffer_event = impl->GetBufferEvent();
+
         IPC::ResponseBuilder rb{ctx, 2, 1};
         rb.Push(ResultSuccess);
-        rb.PushCopyObjects(buffer_event->GetReadableEvent());
+        rb.PushCopyObjects(buffer_event);
     }
 
-    void AppendAudioOutBufferImpl(Kernel::HLERequestContext& ctx) {
-        LOG_DEBUG(Service_Audio, "(STUBBED) called {}", ctx.Description());
-        IPC::RequestParser rp{ctx};
+    void GetReleasedAudioOutBuffers(Kernel::HLERequestContext& ctx) {
+        auto write_buffer_size = ctx.GetWriteBufferSize() / sizeof(u64);
+        std::vector<u64> released_buffers(write_buffer_size, 0);
 
-        const auto& input_buffer{ctx.ReadBuffer()};
-        ASSERT_MSG(input_buffer.size() == sizeof(AudioBuffer),
-                   "AudioBuffer input is an invalid size!");
-        AudioBuffer audio_buffer{};
-        std::memcpy(&audio_buffer, input_buffer.data(), sizeof(AudioBuffer));
-        const u64 tag{rp.Pop<u64>()};
+        auto count = impl->GetReleasedBuffers(released_buffers);
 
-        std::vector<s16> samples(audio_buffer.buffer_size / sizeof(s16));
-        main_memory.ReadBlock(audio_buffer.buffer, samples.data(), audio_buffer.buffer_size);
-
-        if (!audio_core.QueueBuffer(stream, tag, std::move(samples))) {
-            IPC::ResponseBuilder rb{ctx, 2};
-            rb.Push(ERR_BUFFER_COUNT_EXCEEDED);
-            return;
+        [[maybe_unused]] std::string tags{};
+        for (u32 i = 0; i < count; i++) {
+            tags += fmt::format("{:08X}, ", released_buffers[i]);
         }
+        [[maybe_unused]] auto sessionid{impl->GetSystem().GetSessionId()};
+        LOG_TRACE(Service_Audio, "called. Session {} released {} buffers: {}", sessionid, count,
+                  tags);
 
-        IPC::ResponseBuilder rb{ctx, 2};
-        rb.Push(ResultSuccess);
-    }
-
-    void GetReleasedAudioOutBufferImpl(Kernel::HLERequestContext& ctx) {
-        LOG_DEBUG(Service_Audio, "called {}", ctx.Description());
-
-        const u64 max_count{ctx.GetWriteBufferSize() / sizeof(u64)};
-        const auto released_buffers{audio_core.GetTagsAndReleaseBuffers(stream, max_count)};
-
-        std::vector<u64> tags{released_buffers};
-        tags.resize(max_count);
-        ctx.WriteBuffer(tags);
-
+        ctx.WriteBuffer(released_buffers);
         IPC::ResponseBuilder rb{ctx, 3};
         rb.Push(ResultSuccess);
-        rb.Push<u32>(static_cast<u32>(released_buffers.size()));
+        rb.Push(count);
     }
 
     void ContainsAudioOutBuffer(Kernel::HLERequestContext& ctx) {
-        LOG_DEBUG(Service_Audio, "called");
-
         IPC::RequestParser rp{ctx};
+
         const u64 tag{rp.Pop<u64>()};
+        const auto buffer_queued{impl->ContainsAudioBuffer(tag)};
+
+        LOG_DEBUG(Service_Audio, "called. Is buffer {:08X} registered? {}", tag, buffer_queued);
+
         IPC::ResponseBuilder rb{ctx, 3};
         rb.Push(ResultSuccess);
-        rb.Push(stream->ContainsBuffer(tag));
+        rb.Push(buffer_queued);
     }
 
     void GetAudioOutBufferCount(Kernel::HLERequestContext& ctx) {
-        LOG_DEBUG(Service_Audio, "called");
+        const auto buffer_count = impl->GetBufferCount();
+
+        LOG_DEBUG(Service_Audio, "called. Buffer count={}", buffer_count);
 
         IPC::ResponseBuilder rb{ctx, 3};
+
         rb.Push(ResultSuccess);
-        rb.Push(static_cast<u32>(stream->GetQueueSize()));
+        rb.Push(buffer_count);
     }
 
     void GetAudioOutPlayedSampleCount(Kernel::HLERequestContext& ctx) {
-        LOG_DEBUG(Service_Audio, "called");
+        const auto samples_played = impl->GetPlayedSampleCount();
+
+        LOG_DEBUG(Service_Audio, "called. Played samples={}", samples_played);
 
         IPC::ResponseBuilder rb{ctx, 4};
+
         rb.Push(ResultSuccess);
-        rb.Push(stream->GetPlayedSampleCount());
+        rb.Push(samples_played);
     }
 
     void FlushAudioOutBuffers(Kernel::HLERequestContext& ctx) {
-        LOG_DEBUG(Service_Audio, "called");
+        bool flushed{impl->FlushAudioOutBuffers()};
+
+        LOG_DEBUG(Service_Audio, "called. Were any buffers flushed? {}", flushed);
 
         IPC::ResponseBuilder rb{ctx, 3};
         rb.Push(ResultSuccess);
-        rb.Push(stream->Flush());
+        rb.Push(flushed);
     }
 
     void SetAudioOutVolume(Kernel::HLERequestContext& ctx) {
         IPC::RequestParser rp{ctx};
-        const float volume = rp.Pop<float>();
-        LOG_DEBUG(Service_Audio, "called, volume={}", volume);
+        const auto volume = rp.Pop<f32>();
 
-        stream->SetVolume(volume);
+        LOG_DEBUG(Service_Audio, "called. Volume={}", volume);
+
+        impl->SetVolume(volume);
 
         IPC::ResponseBuilder rb{ctx, 2};
         rb.Push(ResultSuccess);
     }
 
     void GetAudioOutVolume(Kernel::HLERequestContext& ctx) {
-        LOG_DEBUG(Service_Audio, "called");
+        const auto volume = impl->GetVolume();
+
+        LOG_DEBUG(Service_Audio, "called. Volume={}", volume);
 
         IPC::ResponseBuilder rb{ctx, 3};
         rb.Push(ResultSuccess);
-        rb.Push(stream->GetVolume());
+        rb.Push(volume);
     }
 
-    AudioCore::AudioOut& audio_core;
-    AudioCore::StreamPtr stream;
-    std::string device_name;
-
-    [[maybe_unused]] AudoutParams audio_params{};
-
-    Core::Memory::Memory& main_memory;
-
     KernelHelpers::ServiceContext service_context;
-
-    /// This is the event handle used to check if the audio buffer was released
-    Kernel::KEvent* buffer_event;
+    Kernel::KEvent* event;
+    std::shared_ptr<AudioCore::AudioOut::Out> impl;
 };
 
-AudOutU::AudOutU(Core::System& system_) : ServiceFramework{system_, "audout:u"} {
+AudOutU::AudOutU(Core::System& system_)
+    : ServiceFramework{system_, "audout:u", ServiceThreadType::CreateNew},
+      service_context{system_, "AudOutU"}, impl{std::make_unique<AudioCore::AudioOut::Manager>(
+                                               system_)} {
     // clang-format off
     static const FunctionInfo functions[] = {
-        {0, &AudOutU::ListAudioOutsImpl, "ListAudioOuts"},
-        {1, &AudOutU::OpenAudioOutImpl, "OpenAudioOut"},
-        {2, &AudOutU::ListAudioOutsImpl, "ListAudioOutsAuto"},
-        {3, &AudOutU::OpenAudioOutImpl, "OpenAudioOutAuto"},
+        {0, &AudOutU::ListAudioOuts, "ListAudioOuts"},
+        {1, &AudOutU::OpenAudioOut, "OpenAudioOut"},
+        {2, &AudOutU::ListAudioOuts, "ListAudioOutsAuto"},
+        {3, &AudOutU::OpenAudioOut, "OpenAudioOutAuto"},
     };
     // clang-format on
 
     RegisterHandlers(functions);
-    audio_core = std::make_unique<AudioCore::AudioOut>();
 }
 
 AudOutU::~AudOutU() = default;
 
-void AudOutU::ListAudioOutsImpl(Kernel::HLERequestContext& ctx) {
-    LOG_DEBUG(Service_Audio, "called");
+void AudOutU::ListAudioOuts(Kernel::HLERequestContext& ctx) {
+    using namespace AudioCore::AudioRenderer;
 
-    ctx.WriteBuffer(DefaultDevice);
+    std::scoped_lock l{impl->mutex};
+
+    const auto write_count =
+        static_cast<u32>(ctx.GetWriteBufferSize() / sizeof(AudioDevice::AudioDeviceName));
+    std::vector<AudioDevice::AudioDeviceName> device_names{};
+    std::string print_names{};
+    if (write_count > 0) {
+        device_names.push_back(AudioDevice::AudioDeviceName("DeviceOut"));
+        LOG_DEBUG(Service_Audio, "called. \nName=DeviceOut");
+    } else {
+        LOG_DEBUG(Service_Audio, "called. Empty buffer passed in.");
+    }
+
+    ctx.WriteBuffer(device_names);
 
     IPC::ResponseBuilder rb{ctx, 3};
     rb.Push(ResultSuccess);
-    rb.Push<u32>(1); // Amount of audio devices
+    rb.Push<u32>(static_cast<u32>(device_names.size()));
 }
 
-void AudOutU::OpenAudioOutImpl(Kernel::HLERequestContext& ctx) {
-    LOG_DEBUG(Service_Audio, "called");
-
-    const auto device_name_data{ctx.ReadBuffer()};
-    std::string device_name;
-    if (device_name_data[0] != '\0') {
-        device_name.assign(device_name_data.begin(), device_name_data.end());
-    } else {
-        device_name.assign(DefaultDevice.begin(), DefaultDevice.end());
-    }
-    ctx.WriteBuffer(device_name);
-
+void AudOutU::OpenAudioOut(Kernel::HLERequestContext& ctx) {
     IPC::RequestParser rp{ctx};
-    auto params{rp.PopRaw<AudoutParams>()};
-    if (params.channel_count <= 2) {
-        // Mono does not exist for audout
-        params.channel_count = 2;
-    } else {
-        params.channel_count = 6;
-    }
-    if (!params.sample_rate) {
-        params.sample_rate = DefaultSampleRate;
+    auto in_params{rp.PopRaw<AudioOutParameter>()};
+    auto applet_resource_user_id{rp.PopRaw<u64>()};
+    const auto device_name_data{ctx.ReadBuffer()};
+    auto device_name = Common::StringFromBuffer(device_name_data);
+    auto handle{ctx.GetCopyHandle(0)};
+
+    auto link{impl->LinkToManager()};
+    if (link.IsError()) {
+        LOG_ERROR(Service_Audio, "Failed to link Audio Out to Audio Manager");
+        IPC::ResponseBuilder rb{ctx, 2};
+        rb.Push(link);
+        return;
     }
 
-    std::string unique_name{fmt::format("{}-{}", device_name, audio_out_interfaces.size())};
-    auto audio_out_interface = std::make_shared<IAudioOut>(
-        system, params, *audio_core, std::move(device_name), std::move(unique_name));
+    size_t new_session_id{};
+    auto result{impl->AcquireSessionId(new_session_id)};
+    if (result.IsError()) {
+        IPC::ResponseBuilder rb{ctx, 2};
+        rb.Push(result);
+        return;
+    }
+
+    LOG_DEBUG(Service_Audio, "Opening new AudioOut, sessionid={}, free sessions={}", new_session_id,
+              impl->num_free_sessions);
+
+    auto audio_out = std::make_shared<IAudioOut>(system, *impl, new_session_id, device_name,
+                                                 in_params, handle, applet_resource_user_id);
+
+    impl->sessions[new_session_id] = audio_out->GetImpl();
+    impl->applet_resource_user_ids[new_session_id] = applet_resource_user_id;
+
+    auto& out_system = impl->sessions[new_session_id]->GetSystem();
+    AudioOutParameterInternal out_params{.sample_rate = out_system.GetSampleRate(),
+                                         .channel_count = out_system.GetChannelCount(),
+                                         .sample_format =
+                                             static_cast<u32>(out_system.GetSampleFormat()),
+                                         .state = static_cast<u32>(out_system.GetState())};
 
     IPC::ResponseBuilder rb{ctx, 6, 0, 1};
-    rb.Push(ResultSuccess);
-    rb.Push<u32>(DefaultSampleRate);
-    rb.Push<u32>(params.channel_count);
-    rb.Push<u32>(static_cast<u32>(AudioCore::Codec::PcmFormat::Int16));
-    rb.Push<u32>(static_cast<u32>(AudioState::Stopped));
-    rb.PushIpcInterface<IAudioOut>(audio_out_interface);
 
-    audio_out_interfaces.push_back(std::move(audio_out_interface));
+    ctx.WriteBuffer(out_system.GetName());
+
+    rb.Push(ResultSuccess);
+    rb.PushRaw<AudioOutParameterInternal>(out_params);
+    rb.PushIpcInterface<IAudioOut>(audio_out);
 }
 
 } // namespace Service::Audio
diff --git a/src/core/hle/service/audio/audout_u.h b/src/core/hle/service/audio/audout_u.h
index d82004c2e1..fdc0ee754e 100644
--- a/src/core/hle/service/audio/audout_u.h
+++ b/src/core/hle/service/audio/audout_u.h
@@ -3,13 +3,11 @@
 
 #pragma once
 
-#include <vector>
+#include "audio_core/audio_out_manager.h"
+#include "audio_core/out/audio_out.h"
+#include "core/hle/service/kernel_helpers.h"
 #include "core/hle/service/service.h"
 
-namespace AudioCore {
-class AudioOut;
-}
-
 namespace Core {
 class System;
 }
@@ -18,6 +16,11 @@ namespace Kernel {
 class HLERequestContext;
 }
 
+namespace AudioCore::AudioOut {
+class Manager;
+class Out;
+} // namespace AudioCore::AudioOut
+
 namespace Service::Audio {
 
 class IAudioOut;
@@ -28,11 +31,11 @@ public:
     ~AudOutU() override;
 
 private:
-    void ListAudioOutsImpl(Kernel::HLERequestContext& ctx);
-    void OpenAudioOutImpl(Kernel::HLERequestContext& ctx);
+    void ListAudioOuts(Kernel::HLERequestContext& ctx);
+    void OpenAudioOut(Kernel::HLERequestContext& ctx);
 
-    std::vector<std::shared_ptr<IAudioOut>> audio_out_interfaces;
-    std::unique_ptr<AudioCore::AudioOut> audio_core;
+    KernelHelpers::ServiceContext service_context;
+    std::unique_ptr<AudioCore::AudioOut::Manager> impl;
 };
 
 } // namespace Service::Audio
diff --git a/src/core/hle/service/audio/audren_u.cpp b/src/core/hle/service/audio/audren_u.cpp
index 2ce63c0041..381a66ba52 100644
--- a/src/core/hle/service/audio/audren_u.cpp
+++ b/src/core/hle/service/audio/audren_u.cpp
@@ -4,7 +4,12 @@
 #include <array>
 #include <memory>
 
-#include "audio_core/audio_renderer.h"
+#include "audio_core/audio_core.h"
+#include "audio_core/common/audio_renderer_parameter.h"
+#include "audio_core/common/feature_support.h"
+#include "audio_core/renderer/audio_device.h"
+#include "audio_core/renderer/audio_renderer.h"
+#include "audio_core/renderer/voice/voice_info.h"
 #include "common/alignment.h"
 #include "common/bit_util.h"
 #include "common/common_funcs.h"
@@ -13,91 +18,112 @@
 #include "core/core.h"
 #include "core/hle/ipc_helpers.h"
 #include "core/hle/kernel/k_event.h"
+#include "core/hle/kernel/k_process.h"
+#include "core/hle/kernel/k_transfer_memory.h"
 #include "core/hle/service/audio/audren_u.h"
 #include "core/hle/service/audio/errors.h"
+#include "core/memory.h"
+
+using namespace AudioCore::AudioRenderer;
 
 namespace Service::Audio {
 
 class IAudioRenderer final : public ServiceFramework<IAudioRenderer> {
 public:
-    explicit IAudioRenderer(Core::System& system_,
-                            const AudioCommon::AudioRendererParameter& audren_params,
-                            const std::size_t instance_number)
+    explicit IAudioRenderer(Core::System& system_, Manager& manager_,
+                            AudioCore::AudioRendererParameterInternal& params,
+                            Kernel::KTransferMemory* transfer_memory, u64 transfer_memory_size,
+                            u32 process_handle, u64 applet_resource_user_id, s32 session_id)
         : ServiceFramework{system_, "IAudioRenderer", ServiceThreadType::CreateNew},
-          service_context{system_, "IAudioRenderer"} {
+          service_context{system_, "IAudioRenderer"}, rendered_event{service_context.CreateEvent(
+                                                          "IAudioRendererEvent")},
+          manager{manager_}, impl{std::make_unique<Renderer>(system_, manager, rendered_event)} {
         // clang-format off
         static const FunctionInfo functions[] = {
             {0, &IAudioRenderer::GetSampleRate, "GetSampleRate"},
             {1, &IAudioRenderer::GetSampleCount, "GetSampleCount"},
             {2, &IAudioRenderer::GetMixBufferCount, "GetMixBufferCount"},
             {3, &IAudioRenderer::GetState, "GetState"},
-            {4, &IAudioRenderer::RequestUpdateImpl, "RequestUpdate"},
+            {4, &IAudioRenderer::RequestUpdate, "RequestUpdate"},
             {5, &IAudioRenderer::Start, "Start"},
             {6, &IAudioRenderer::Stop, "Stop"},
             {7, &IAudioRenderer::QuerySystemEvent, "QuerySystemEvent"},
             {8, &IAudioRenderer::SetRenderingTimeLimit, "SetRenderingTimeLimit"},
             {9, &IAudioRenderer::GetRenderingTimeLimit, "GetRenderingTimeLimit"},
-            {10, &IAudioRenderer::RequestUpdateImpl, "RequestUpdateAuto"},
-            {11, &IAudioRenderer::ExecuteAudioRendererRendering, "ExecuteAudioRendererRendering"},
+            {10, nullptr, "RequestUpdateAuto"},
+            {11, nullptr, "ExecuteAudioRendererRendering"},
         };
         // clang-format on
         RegisterHandlers(functions);
 
-        system_event = service_context.CreateEvent("IAudioRenderer:SystemEvent");
-        renderer = std::make_unique<AudioCore::AudioRenderer>(
-            system.CoreTiming(), system.Memory(), audren_params,
-            [this]() {
-                const auto guard = LockService();
-                system_event->GetWritableEvent().Signal();
-            },
-            instance_number);
+        impl->Initialize(params, transfer_memory, transfer_memory_size, process_handle,
+                         applet_resource_user_id, session_id);
     }
 
     ~IAudioRenderer() override {
-        service_context.CloseEvent(system_event);
+        impl->Finalize();
+        service_context.CloseEvent(rendered_event);
     }
 
 private:
     void GetSampleRate(Kernel::HLERequestContext& ctx) {
-        LOG_DEBUG(Service_Audio, "called");
+        const auto sample_rate{impl->GetSystem().GetSampleRate()};
+
+        LOG_DEBUG(Service_Audio, "called. Sample rate {}", sample_rate);
 
         IPC::ResponseBuilder rb{ctx, 3};
         rb.Push(ResultSuccess);
-        rb.Push<u32>(renderer->GetSampleRate());
+        rb.Push(sample_rate);
     }
 
     void GetSampleCount(Kernel::HLERequestContext& ctx) {
-        LOG_DEBUG(Service_Audio, "called");
+        const auto sample_count{impl->GetSystem().GetSampleCount()};
+
+        LOG_DEBUG(Service_Audio, "called. Sample count {}", sample_count);
 
         IPC::ResponseBuilder rb{ctx, 3};
         rb.Push(ResultSuccess);
-        rb.Push<u32>(renderer->GetSampleCount());
+        rb.Push(sample_count);
     }
 
     void GetState(Kernel::HLERequestContext& ctx) {
-        LOG_DEBUG(Service_Audio, "called");
+        const u32 state{!impl->GetSystem().IsActive()};
+
+        LOG_DEBUG(Service_Audio, "called, state {}", state);
 
         IPC::ResponseBuilder rb{ctx, 3};
         rb.Push(ResultSuccess);
-        rb.Push<u32>(static_cast<u32>(renderer->GetStreamState()));
+        rb.Push(state);
     }
 
     void GetMixBufferCount(Kernel::HLERequestContext& ctx) {
         LOG_DEBUG(Service_Audio, "called");
 
+        const auto buffer_count{impl->GetSystem().GetMixBufferCount()};
+
         IPC::ResponseBuilder rb{ctx, 3};
         rb.Push(ResultSuccess);
-        rb.Push<u32>(renderer->GetMixBufferCount());
+        rb.Push(buffer_count);
     }
 
-    void RequestUpdateImpl(Kernel::HLERequestContext& ctx) {
-        LOG_DEBUG(Service_Audio, "(STUBBED) called");
+    void RequestUpdate(Kernel::HLERequestContext& ctx) {
+        LOG_TRACE(Service_Audio, "called");
 
-        std::vector<u8> output_params(ctx.GetWriteBufferSize(), 0);
-        auto result = renderer->UpdateAudioRenderer(ctx.ReadBuffer(), output_params);
+        std::vector<u8> input{ctx.ReadBuffer(0)};
+
+        // These buffers are written manually to avoid an issue with WriteBuffer throwing errors for
+        // checking size 0. Performance size is 0 for most games.
+        const auto buffers{ctx.BufferDescriptorB()};
+        std::vector<u8> output(buffers[0].Size(), 0);
+        std::vector<u8> performance(buffers[1].Size(), 0);
+
+        auto result = impl->RequestUpdate(input, performance, output);
 
         if (result.IsSuccess()) {
-            ctx.WriteBuffer(output_params);
+            ctx.WriteBufferB(output.data(), output.size(), 0);
+            ctx.WriteBufferB(performance.data(), performance.size(), 1);
+        } else {
+            LOG_ERROR(Service_Audio, "RequestUpdate failed error 0x{:02X}!", result.description);
         }
 
         IPC::ResponseBuilder rb{ctx, 2};
@@ -105,38 +131,45 @@ private:
     }
 
     void Start(Kernel::HLERequestContext& ctx) {
-        LOG_WARNING(Service_Audio, "(STUBBED) called");
+        LOG_DEBUG(Service_Audio, "called");
 
-        const auto result = renderer->Start();
+        impl->Start();
 
         IPC::ResponseBuilder rb{ctx, 2};
-        rb.Push(result);
+        rb.Push(ResultSuccess);
     }
 
     void Stop(Kernel::HLERequestContext& ctx) {
-        LOG_WARNING(Service_Audio, "(STUBBED) called");
+        LOG_DEBUG(Service_Audio, "called");
 
-        const auto result = renderer->Stop();
+        impl->Stop();
 
         IPC::ResponseBuilder rb{ctx, 2};
-        rb.Push(result);
+        rb.Push(ResultSuccess);
     }
 
     void QuerySystemEvent(Kernel::HLERequestContext& ctx) {
-        LOG_WARNING(Service_Audio, "(STUBBED) called");
+        LOG_DEBUG(Service_Audio, "called");
+
+        if (impl->GetSystem().GetExecutionMode() == AudioCore::ExecutionMode::Manual) {
+            IPC::ResponseBuilder rb{ctx, 2};
+            rb.Push(ERR_NOT_SUPPORTED);
+            return;
+        }
 
         IPC::ResponseBuilder rb{ctx, 2, 1};
         rb.Push(ResultSuccess);
-        rb.PushCopyObjects(system_event->GetReadableEvent());
+        rb.PushCopyObjects(rendered_event->GetReadableEvent());
     }
 
     void SetRenderingTimeLimit(Kernel::HLERequestContext& ctx) {
-        IPC::RequestParser rp{ctx};
-        rendering_time_limit_percent = rp.Pop<u32>();
-        LOG_DEBUG(Service_Audio, "called. rendering_time_limit_percent={}",
-                  rendering_time_limit_percent);
+        LOG_DEBUG(Service_Audio, "called");
 
-        ASSERT(rendering_time_limit_percent <= 100);
+        IPC::RequestParser rp{ctx};
+        auto limit = rp.PopRaw<u32>();
+
+        auto& system_ = impl->GetSystem();
+        system_.SetRenderingTimeLimit(limit);
 
         IPC::ResponseBuilder rb{ctx, 2};
         rb.Push(ResultSuccess);
@@ -145,34 +178,34 @@ private:
     void GetRenderingTimeLimit(Kernel::HLERequestContext& ctx) {
         LOG_DEBUG(Service_Audio, "called");
 
+        auto& system_ = impl->GetSystem();
+        auto time = system_.GetRenderingTimeLimit();
+
         IPC::ResponseBuilder rb{ctx, 3};
         rb.Push(ResultSuccess);
-        rb.Push(rendering_time_limit_percent);
+        rb.Push(time);
     }
 
     void ExecuteAudioRendererRendering(Kernel::HLERequestContext& ctx) {
         LOG_DEBUG(Service_Audio, "called");
-
-        // This service command currently only reports an unsupported operation
-        // error code, or aborts. Given that, we just always return an error
-        // code in this case.
-
-        IPC::ResponseBuilder rb{ctx, 2};
-        rb.Push(ERR_NOT_SUPPORTED);
     }
 
     KernelHelpers::ServiceContext service_context;
-
-    Kernel::KEvent* system_event;
-    std::unique_ptr<AudioCore::AudioRenderer> renderer;
-    u32 rendering_time_limit_percent = 100;
+    Kernel::KEvent* rendered_event;
+    Manager& manager;
+    std::unique_ptr<Renderer> impl;
 };
 
 class IAudioDevice final : public ServiceFramework<IAudioDevice> {
+
 public:
-    explicit IAudioDevice(Core::System& system_, Kernel::KEvent* buffer_event_, u32_le revision_)
-        : ServiceFramework{system_, "IAudioDevice"}, buffer_event{buffer_event_}, revision{
-                                                                                      revision_} {
+    explicit IAudioDevice(Core::System& system_, u64 applet_resource_user_id, u32 revision,
+                          u32 device_num)
+        : ServiceFramework{system_, "IAudioDevice", ServiceThreadType::CreateNew},
+          service_context{system_, "IAudioDevice"}, impl{std::make_unique<AudioDevice>(
+                                                        system_, applet_resource_user_id,
+                                                        revision)},
+          event{service_context.CreateEvent(fmt::format("IAudioDeviceEvent-{}", device_num))} {
         static const FunctionInfo functions[] = {
             {0, &IAudioDevice::ListAudioDeviceName, "ListAudioDeviceName"},
             {1, &IAudioDevice::SetAudioDeviceOutputVolume, "SetAudioDeviceOutputVolume"},
@@ -186,54 +219,45 @@ public:
             {10, &IAudioDevice::GetActiveAudioDeviceName, "GetActiveAudioDeviceNameAuto"},
             {11, &IAudioDevice::QueryAudioDeviceInputEvent, "QueryAudioDeviceInputEvent"},
             {12, &IAudioDevice::QueryAudioDeviceOutputEvent, "QueryAudioDeviceOutputEvent"},
-            {13, nullptr, "GetActiveAudioOutputDeviceName"},
-            {14, nullptr, "ListAudioOutputDeviceName"},
+            {13, &IAudioDevice::GetActiveAudioDeviceName, "GetActiveAudioOutputDeviceName"},
+            {14, &IAudioDevice::ListAudioOutputDeviceName, "ListAudioOutputDeviceName"},
         };
         RegisterHandlers(functions);
+
+        event->GetWritableEvent().Signal();
+    }
+
+    ~IAudioDevice() override {
+        service_context.CloseEvent(event);
     }
 
 private:
-    using AudioDeviceName = std::array<char, 256>;
-    static constexpr std::array<std::string_view, 4> audio_device_names{{
-        "AudioStereoJackOutput",
-        "AudioBuiltInSpeakerOutput",
-        "AudioTvOutput",
-        "AudioUsbDeviceOutput",
-    }};
-    enum class DeviceType {
-        AHUBHeadphones,
-        AHUBSpeakers,
-        HDA,
-        USBOutput,
-    };
-
     void ListAudioDeviceName(Kernel::HLERequestContext& ctx) {
-        LOG_DEBUG(Service_Audio, "called");
+        const size_t in_count = ctx.GetWriteBufferSize() / sizeof(AudioDevice::AudioDeviceName);
 
-        const bool usb_output_supported =
-            IsFeatureSupported(AudioFeatures::AudioUSBDeviceOutput, revision);
-        const std::size_t count = ctx.GetWriteBufferSize() / sizeof(AudioDeviceName);
+        std::vector<AudioDevice::AudioDeviceName> out_names{};
 
-        std::vector<AudioDeviceName> name_buffer;
-        name_buffer.reserve(audio_device_names.size());
+        u32 out_count = impl->ListAudioDeviceName(out_names, in_count);
 
-        for (std::size_t i = 0; i < count && i < audio_device_names.size(); i++) {
-            const auto type = static_cast<DeviceType>(i);
-
-            if (!usb_output_supported && type == DeviceType::USBOutput) {
-                continue;
+        std::string out{};
+        for (u32 i = 0; i < out_count; i++) {
+            std::string a{};
+            u32 j = 0;
+            while (out_names[i].name[j] != '\0') {
+                a += out_names[i].name[j];
+                j++;
             }
-
-            const auto& device_name = audio_device_names[i];
-            auto& entry = name_buffer.emplace_back();
-            device_name.copy(entry.data(), device_name.size());
+            out += "\n\t" + a;
         }
 
-        ctx.WriteBuffer(name_buffer);
+        LOG_DEBUG(Service_Audio, "called.\nNames={}", out);
 
         IPC::ResponseBuilder rb{ctx, 3};
+
+        ctx.WriteBuffer(out_names);
+
         rb.Push(ResultSuccess);
-        rb.Push(static_cast<u32>(name_buffer.size()));
+        rb.Push(out_count);
     }
 
     void SetAudioDeviceOutputVolume(Kernel::HLERequestContext& ctx) {
@@ -243,7 +267,11 @@ private:
         const auto device_name_buffer = ctx.ReadBuffer();
         const std::string name = Common::StringFromBuffer(device_name_buffer);
 
-        LOG_WARNING(Service_Audio, "(STUBBED) called. name={}, volume={}", name, volume);
+        LOG_DEBUG(Service_Audio, "called. name={}, volume={}", name, volume);
+
+        if (name == "AudioTvOutput") {
+            impl->SetDeviceVolumes(volume);
+        }
 
         IPC::ResponseBuilder rb{ctx, 2};
         rb.Push(ResultSuccess);
@@ -253,53 +281,60 @@ private:
         const auto device_name_buffer = ctx.ReadBuffer();
         const std::string name = Common::StringFromBuffer(device_name_buffer);
 
-        LOG_WARNING(Service_Audio, "(STUBBED) called. name={}", name);
+        LOG_DEBUG(Service_Audio, "called. Name={}", name);
+
+        f32 volume{1.0f};
+        if (name == "AudioTvOutput") {
+            volume = impl->GetDeviceVolume(name);
+        }
 
         IPC::ResponseBuilder rb{ctx, 3};
         rb.Push(ResultSuccess);
-        rb.Push(1.0f);
+        rb.Push(volume);
     }
 
     void GetActiveAudioDeviceName(Kernel::HLERequestContext& ctx) {
-        LOG_WARNING(Service_Audio, "(STUBBED) called");
+        const auto write_size = ctx.GetWriteBufferSize() / sizeof(char);
+        std::string out_name{"AudioTvOutput"};
 
-        // Currently set to always be TV audio output.
-        const auto& device_name = audio_device_names[2];
+        LOG_DEBUG(Service_Audio, "(STUBBED) called. Name={}", out_name);
 
-        AudioDeviceName out_device_name{};
-        device_name.copy(out_device_name.data(), device_name.size());
+        out_name.resize(write_size);
 
-        ctx.WriteBuffer(out_device_name);
+        ctx.WriteBuffer(out_name);
 
         IPC::ResponseBuilder rb{ctx, 2};
         rb.Push(ResultSuccess);
     }
 
     void QueryAudioDeviceSystemEvent(Kernel::HLERequestContext& ctx) {
-        LOG_WARNING(Service_Audio, "(STUBBED) called");
+        LOG_DEBUG(Service_Audio, "(STUBBED) called");
 
-        buffer_event->GetWritableEvent().Signal();
+        event->GetWritableEvent().Signal();
 
         IPC::ResponseBuilder rb{ctx, 2, 1};
         rb.Push(ResultSuccess);
-        rb.PushCopyObjects(buffer_event->GetReadableEvent());
+        rb.PushCopyObjects(event->GetReadableEvent());
     }
 
     void GetActiveChannelCount(Kernel::HLERequestContext& ctx) {
-        LOG_WARNING(Service_Audio, "(STUBBED) called");
+        const auto& sink{system.AudioCore().GetOutputSink()};
+        u32 channel_count{sink.GetDeviceChannels()};
+
+        LOG_DEBUG(Service_Audio, "(STUBBED) called. Channels={}", channel_count);
 
         IPC::ResponseBuilder rb{ctx, 3};
+
         rb.Push(ResultSuccess);
-        rb.Push<u32>(2);
+        rb.Push<u32>(channel_count);
     }
 
-    // Should be similar to QueryAudioDeviceOutputEvent
     void QueryAudioDeviceInputEvent(Kernel::HLERequestContext& ctx) {
-        LOG_WARNING(Service_Audio, "(STUBBED) called");
+        LOG_DEBUG(Service_Audio, "(STUBBED) called");
 
         IPC::ResponseBuilder rb{ctx, 2, 1};
         rb.Push(ResultSuccess);
-        rb.PushCopyObjects(buffer_event->GetReadableEvent());
+        rb.PushCopyObjects(event->GetReadableEvent());
     }
 
     void QueryAudioDeviceOutputEvent(Kernel::HLERequestContext& ctx) {
@@ -307,402 +342,167 @@ private:
 
         IPC::ResponseBuilder rb{ctx, 2, 1};
         rb.Push(ResultSuccess);
-        rb.PushCopyObjects(buffer_event->GetReadableEvent());
+        rb.PushCopyObjects(event->GetReadableEvent());
     }
 
-    Kernel::KEvent* buffer_event;
-    u32_le revision = 0;
+    void ListAudioOutputDeviceName(Kernel::HLERequestContext& ctx) {
+        const size_t in_count = ctx.GetWriteBufferSize() / sizeof(AudioDevice::AudioDeviceName);
+
+        std::vector<AudioDevice::AudioDeviceName> out_names{};
+
+        u32 out_count = impl->ListAudioOutputDeviceName(out_names, in_count);
+
+        std::string out{};
+        for (u32 i = 0; i < out_count; i++) {
+            std::string a{};
+            u32 j = 0;
+            while (out_names[i].name[j] != '\0') {
+                a += out_names[i].name[j];
+                j++;
+            }
+            out += "\n\t" + a;
+        }
+
+        LOG_DEBUG(Service_Audio, "called.\nNames={}", out);
+
+        IPC::ResponseBuilder rb{ctx, 3};
+
+        ctx.WriteBuffer(out_names);
+
+        rb.Push(ResultSuccess);
+        rb.Push(out_count);
+    }
+
+    KernelHelpers::ServiceContext service_context;
+    std::unique_ptr<AudioDevice> impl;
+    Kernel::KEvent* event;
 };
 
 AudRenU::AudRenU(Core::System& system_)
-    : ServiceFramework{system_, "audren:u"}, service_context{system_, "audren:u"} {
+    : ServiceFramework{system_, "audren:u", ServiceThreadType::CreateNew},
+      service_context{system_, "audren:u"}, impl{std::make_unique<Manager>(system_)} {
     // clang-format off
     static const FunctionInfo functions[] = {
         {0, &AudRenU::OpenAudioRenderer, "OpenAudioRenderer"},
-        {1, &AudRenU::GetAudioRendererWorkBufferSize, "GetWorkBufferSize"},
+        {1, &AudRenU::GetWorkBufferSize, "GetWorkBufferSize"},
         {2, &AudRenU::GetAudioDeviceService, "GetAudioDeviceService"},
-        {3, &AudRenU::OpenAudioRendererForManualExecution, "OpenAudioRendererForManualExecution"},
+        {3, nullptr, "OpenAudioRendererForManualExecution"},
         {4, &AudRenU::GetAudioDeviceServiceWithRevisionInfo, "GetAudioDeviceServiceWithRevisionInfo"},
     };
     // clang-format on
 
     RegisterHandlers(functions);
-
-    buffer_event = service_context.CreateEvent("IAudioOutBufferReleasedEvent");
 }
 
-AudRenU::~AudRenU() {
-    service_context.CloseEvent(buffer_event);
-}
+AudRenU::~AudRenU() = default;
 
 void AudRenU::OpenAudioRenderer(Kernel::HLERequestContext& ctx) {
-    LOG_DEBUG(Service_Audio, "called");
+    IPC::RequestParser rp{ctx};
 
-    OpenAudioRendererImpl(ctx);
+    AudioCore::AudioRendererParameterInternal params;
+    rp.PopRaw<AudioCore::AudioRendererParameterInternal>(params);
+    auto transfer_memory_handle = ctx.GetCopyHandle(0);
+    auto process_handle = ctx.GetCopyHandle(1);
+    auto transfer_memory_size = rp.Pop<u64>();
+    auto applet_resource_user_id = rp.Pop<u64>();
+
+    if (impl->GetSessionCount() + 1 > AudioCore::MaxRendererSessions) {
+        LOG_ERROR(Service_Audio, "Too many AudioRenderer sessions open!");
+        IPC::ResponseBuilder rb{ctx, 2};
+        rb.Push(ERR_MAXIMUM_SESSIONS_REACHED);
+        return;
+    }
+
+    const auto& handle_table{system.CurrentProcess()->GetHandleTable()};
+    auto process{handle_table.GetObject<Kernel::KProcess>(process_handle)};
+    auto transfer_memory{
+        process->GetHandleTable().GetObject<Kernel::KTransferMemory>(transfer_memory_handle)};
+
+    const auto session_id{impl->GetSessionId()};
+    if (session_id == -1) {
+        LOG_ERROR(Service_Audio, "Tried to open a session that's already in use!");
+        IPC::ResponseBuilder rb{ctx, 2};
+        rb.Push(ERR_MAXIMUM_SESSIONS_REACHED);
+        return;
+    }
+
+    LOG_DEBUG(Service_Audio, "Opened new AudioRenderer session {} sessions open {}", session_id,
+              impl->GetSessionCount());
+
+    IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+    rb.Push(ResultSuccess);
+    rb.PushIpcInterface<IAudioRenderer>(system, *impl, params, transfer_memory.GetPointerUnsafe(),
+                                        transfer_memory_size, process_handle,
+                                        applet_resource_user_id, session_id);
 }
 
-static u64 CalculateNumPerformanceEntries(const AudioCommon::AudioRendererParameter& params) {
-    // +1 represents the final mix.
-    return u64{params.effect_count} + params.submix_count + params.sink_count + params.voice_count +
-           1;
-}
-
-void AudRenU::GetAudioRendererWorkBufferSize(Kernel::HLERequestContext& ctx) {
-    LOG_DEBUG(Service_Audio, "called");
-
-    // Several calculations below align the sizes being calculated
-    // onto a 64 byte boundary.
-    static constexpr u64 buffer_alignment_size = 64;
-
-    // Some calculations that calculate portions of the buffer
-    // that will contain information, on the other hand, align
-    // the result of some of their calcularions on a 16 byte boundary.
-    static constexpr u64 info_field_alignment_size = 16;
-
-    // Maximum detail entries that may exist at one time for performance
-    // frame statistics.
-    static constexpr u64 max_perf_detail_entries = 100;
-
-    // Size of the data structure representing the bulk of the voice-related state.
-    static constexpr u64 voice_state_size_bytes = 0x100;
-
-    // Size of the upsampler manager data structure
-    constexpr u64 upsampler_manager_size = 0x48;
-
-    // Calculates the part of the size that relates to mix buffers.
-    const auto calculate_mix_buffer_sizes = [](const AudioCommon::AudioRendererParameter& params) {
-        // As of 8.0.0 this is the maximum on voice channels.
-        constexpr u64 max_voice_channels = 6;
-
-        // The service expects the sample_count member of the parameters to either be
-        // a value of 160 or 240, so the maximum sample count is assumed in order
-        // to adequately handle all values at runtime.
-        constexpr u64 default_max_sample_count = 240;
-
-        const u64 total_mix_buffers = params.mix_buffer_count + max_voice_channels;
-
-        u64 size = 0;
-        size += total_mix_buffers * (sizeof(s32) * params.sample_count);
-        size += total_mix_buffers * (sizeof(s32) * default_max_sample_count);
-        size += u64{params.submix_count} + params.sink_count;
-        size = Common::AlignUp(size, buffer_alignment_size);
-        size += Common::AlignUp(params.unknown_30, buffer_alignment_size);
-        size += Common::AlignUp(sizeof(s32) * params.mix_buffer_count, buffer_alignment_size);
-        return size;
-    };
-
-    // Calculates the portion of the size related to the mix data (and the sorting thereof).
-    const auto calculate_mix_info_size = [](const AudioCommon::AudioRendererParameter& params) {
-        // The size of the mixing info data structure.
-        constexpr u64 mix_info_size = 0x940;
-
-        // Consists of total submixes with the final mix included.
-        const u64 total_mix_count = u64{params.submix_count} + 1;
-
-        // The total number of effects that may be available to the audio renderer at any time.
-        constexpr u64 max_effects = 256;
-
-        // Calculates the part of the size related to the audio node state.
-        // This will only be used if the audio revision supports the splitter.
-        const auto calculate_node_state_size = [](std::size_t num_nodes) {
-            // Internally within a nodestate, it appears to use a data structure
-            // similar to a std::bitset<64> twice.
-            constexpr u64 bit_size = Common::BitSize<u64>();
-            constexpr u64 num_bitsets = 2;
-
-            // Node state instances have three states internally for performing
-            // depth-first searches of nodes. Initialized, Found, and Done Sorting.
-            constexpr u64 num_states = 3;
-
-            u64 size = 0;
-            size += (num_nodes * num_nodes) * sizeof(s32);
-            size += num_states * (num_nodes * sizeof(s32));
-            size += num_bitsets * (Common::AlignUp(num_nodes, bit_size) / Common::BitSize<u8>());
-            return size;
-        };
-
-        // Calculates the part of the size related to the adjacency (aka edge) matrix.
-        const auto calculate_edge_matrix_size = [](std::size_t num_nodes) {
-            return (num_nodes * num_nodes) * sizeof(s32);
-        };
-
-        u64 size = 0;
-        size += Common::AlignUp(sizeof(void*) * total_mix_count, info_field_alignment_size);
-        size += Common::AlignUp(mix_info_size * total_mix_count, info_field_alignment_size);
-        size += Common::AlignUp(sizeof(s32) * max_effects * params.submix_count,
-                                info_field_alignment_size);
-
-        if (IsFeatureSupported(AudioFeatures::Splitter, params.revision)) {
-            size += Common::AlignUp(calculate_node_state_size(total_mix_count) +
-                                        calculate_edge_matrix_size(total_mix_count),
-                                    info_field_alignment_size);
-        }
-
-        return size;
-    };
-
-    // Calculates the part of the size related to voice channel info.
-    const auto calculate_voice_info_size = [](const AudioCommon::AudioRendererParameter& params) {
-        constexpr u64 voice_info_size = 0x220;
-        constexpr u64 voice_resource_size = 0xD0;
-
-        u64 size = 0;
-        size += Common::AlignUp(sizeof(void*) * params.voice_count, info_field_alignment_size);
-        size += Common::AlignUp(voice_info_size * params.voice_count, info_field_alignment_size);
-        size +=
-            Common::AlignUp(voice_resource_size * params.voice_count, info_field_alignment_size);
-        size +=
-            Common::AlignUp(voice_state_size_bytes * params.voice_count, info_field_alignment_size);
-        return size;
-    };
-
-    // Calculates the part of the size related to memory pools.
-    const auto calculate_memory_pools_size = [](const AudioCommon::AudioRendererParameter& params) {
-        const u64 num_memory_pools = sizeof(s32) * (u64{params.effect_count} + params.voice_count);
-        const u64 memory_pool_info_size = 0x20;
-        return Common::AlignUp(num_memory_pools * memory_pool_info_size, info_field_alignment_size);
-    };
-
-    // Calculates the part of the size related to the splitter context.
-    const auto calculate_splitter_context_size =
-        [](const AudioCommon::AudioRendererParameter& params) -> u64 {
-        if (!IsFeatureSupported(AudioFeatures::Splitter, params.revision)) {
-            return 0;
-        }
-
-        constexpr u64 splitter_info_size = 0x20;
-        constexpr u64 splitter_destination_data_size = 0xE0;
-
-        u64 size = 0;
-        size += params.num_splitter_send_channels;
-        size +=
-            Common::AlignUp(splitter_info_size * params.splitter_count, info_field_alignment_size);
-        size += Common::AlignUp(splitter_destination_data_size * params.num_splitter_send_channels,
-                                info_field_alignment_size);
-
-        return size;
-    };
-
-    // Calculates the part of the size related to the upsampler info.
-    const auto calculate_upsampler_info_size =
-        [](const AudioCommon::AudioRendererParameter& params) {
-            constexpr u64 upsampler_info_size = 0x280;
-            // Yes, using the buffer size over info alignment size is intentional here.
-            return Common::AlignUp(upsampler_info_size *
-                                       (u64{params.submix_count} + params.sink_count),
-                                   buffer_alignment_size);
-        };
-
-    // Calculates the part of the size related to effect info.
-    const auto calculate_effect_info_size = [](const AudioCommon::AudioRendererParameter& params) {
-        constexpr u64 effect_info_size = 0x2B0;
-        return Common::AlignUp(effect_info_size * params.effect_count, info_field_alignment_size);
-    };
-
-    // Calculates the part of the size related to audio sink info.
-    const auto calculate_sink_info_size = [](const AudioCommon::AudioRendererParameter& params) {
-        const u64 sink_info_size = 0x170;
-        return Common::AlignUp(sink_info_size * params.sink_count, info_field_alignment_size);
-    };
-
-    // Calculates the part of the size related to voice state info.
-    const auto calculate_voice_state_size = [](const AudioCommon::AudioRendererParameter& params) {
-        const u64 voice_state_size = 0x100;
-        const u64 additional_size = buffer_alignment_size - 1;
-        return Common::AlignUp(voice_state_size * params.voice_count + additional_size,
-                               info_field_alignment_size);
-    };
-
-    // Calculates the part of the size related to performance statistics.
-    const auto calculate_perf_size = [](const AudioCommon::AudioRendererParameter& params) {
-        // Extra size value appended to the end of the calculation.
-        constexpr u64 appended = 128;
-
-        // Whether or not we assume the newer version of performance metrics data structures.
-        const bool is_v2 =
-            IsFeatureSupported(AudioFeatures::PerformanceMetricsVersion2, params.revision);
-
-        // Data structure sizes
-        constexpr u64 perf_statistics_size = 0x0C;
-        const u64 header_size = is_v2 ? 0x30 : 0x18;
-        const u64 entry_size = is_v2 ? 0x18 : 0x10;
-        const u64 detail_size = is_v2 ? 0x18 : 0x10;
-
-        const u64 entry_count = CalculateNumPerformanceEntries(params);
-        const u64 size_per_frame =
-            header_size + (entry_size * entry_count) + (detail_size * max_perf_detail_entries);
-
-        u64 size = 0;
-        size += Common::AlignUp(size_per_frame * params.performance_frame_count + 1,
-                                buffer_alignment_size);
-        size += Common::AlignUp(perf_statistics_size, buffer_alignment_size);
-        size += appended;
-        return size;
-    };
-
-    // Calculates the part of the size that relates to the audio command buffer.
-    const auto calculate_command_buffer_size =
-        [](const AudioCommon::AudioRendererParameter& params) {
-            constexpr u64 alignment = (buffer_alignment_size - 1) * 2;
-
-            if (!IsFeatureSupported(AudioFeatures::VariadicCommandBuffer, params.revision)) {
-                constexpr u64 command_buffer_size = 0x18000;
-
-                return command_buffer_size + alignment;
-            }
-
-            // When the variadic command buffer is supported, this means
-            // the command generator for the audio renderer can issue commands
-            // that are (as one would expect), variable in size. So what we need to do
-            // is determine the maximum possible size for a few command data structures
-            // then multiply them by the amount of present commands indicated by the given
-            // respective audio parameters.
-
-            constexpr u64 max_biquad_filters = 2;
-            constexpr u64 max_mix_buffers = 24;
-
-            constexpr u64 biquad_filter_command_size = 0x2C;
-
-            constexpr u64 depop_mix_command_size = 0x24;
-            constexpr u64 depop_setup_command_size = 0x50;
-
-            constexpr u64 effect_command_max_size = 0x540;
-
-            constexpr u64 mix_command_size = 0x1C;
-            constexpr u64 mix_ramp_command_size = 0x24;
-            constexpr u64 mix_ramp_grouped_command_size = 0x13C;
-
-            constexpr u64 perf_command_size = 0x28;
-
-            constexpr u64 sink_command_size = 0x130;
-
-            constexpr u64 submix_command_max_size =
-                depop_mix_command_size + (mix_command_size * max_mix_buffers) * max_mix_buffers;
-
-            constexpr u64 volume_command_size = 0x1C;
-            constexpr u64 volume_ramp_command_size = 0x20;
-
-            constexpr u64 voice_biquad_filter_command_size =
-                biquad_filter_command_size * max_biquad_filters;
-            constexpr u64 voice_data_command_size = 0x9C;
-            const u64 voice_command_max_size =
-                (params.splitter_count * depop_setup_command_size) +
-                (voice_data_command_size + voice_biquad_filter_command_size +
-                 volume_ramp_command_size + mix_ramp_grouped_command_size);
-
-            // Now calculate the individual elements that comprise the size and add them together.
-            const u64 effect_commands_size = params.effect_count * effect_command_max_size;
-
-            const u64 final_mix_commands_size =
-                depop_mix_command_size + volume_command_size * max_mix_buffers;
-
-            const u64 perf_commands_size =
-                perf_command_size *
-                (CalculateNumPerformanceEntries(params) + max_perf_detail_entries);
-
-            const u64 sink_commands_size = params.sink_count * sink_command_size;
-
-            const u64 splitter_commands_size =
-                params.num_splitter_send_channels * max_mix_buffers * mix_ramp_command_size;
-
-            const u64 submix_commands_size = params.submix_count * submix_command_max_size;
-
-            const u64 voice_commands_size = params.voice_count * voice_command_max_size;
-
-            return effect_commands_size + final_mix_commands_size + perf_commands_size +
-                   sink_commands_size + splitter_commands_size + submix_commands_size +
-                   voice_commands_size + alignment;
-        };
+void AudRenU::GetWorkBufferSize(Kernel::HLERequestContext& ctx) {
+    AudioCore::AudioRendererParameterInternal params;
 
     IPC::RequestParser rp{ctx};
-    const auto params = rp.PopRaw<AudioCommon::AudioRendererParameter>();
+    rp.PopRaw<AudioCore::AudioRendererParameterInternal>(params);
 
-    u64 size = 0;
-    size += calculate_mix_buffer_sizes(params);
-    size += calculate_mix_info_size(params);
-    size += calculate_voice_info_size(params);
-    size += upsampler_manager_size;
-    size += calculate_memory_pools_size(params);
-    size += calculate_splitter_context_size(params);
+    u64 size{0};
+    auto result = impl->GetWorkBufferSize(params, size);
 
-    size = Common::AlignUp(size, buffer_alignment_size);
+    std::string output_info{};
+    output_info += fmt::format("\tRevision {}", AudioCore::GetRevisionNum(params.revision));
+    output_info +=
+        fmt::format("\n\tSample Rate {}, Sample Count {}", params.sample_rate, params.sample_count);
+    output_info += fmt::format("\n\tExecution Mode {}, Voice Drop Enabled {}",
+                               static_cast<u32>(params.execution_mode), params.voice_drop_enabled);
+    output_info += fmt::format(
+        "\n\tSizes: Effects {:04X}, Mixes {:04X}, Sinks {:04X}, Submixes {:04X}, Splitter Infos "
+        "{:04X}, Splitter Destinations {:04X}, Voices {:04X}, Performance Frames {:04X} External "
+        "Context {:04X}",
+        params.effects, params.mixes, params.sinks, params.sub_mixes, params.splitter_infos,
+        params.splitter_destinations, params.voices, params.perf_frames,
+        params.external_context_size);
 
-    size += calculate_upsampler_info_size(params);
-    size += calculate_effect_info_size(params);
-    size += calculate_sink_info_size(params);
-    size += calculate_voice_state_size(params);
-    size += calculate_perf_size(params);
-    size += calculate_command_buffer_size(params);
-
-    // finally, 4KB page align the size, and we're done.
-    size = Common::AlignUp(size, 4096);
+    LOG_DEBUG(Service_Audio, "called.\nInput params:\n{}\nOutput params:\n\tWorkbuffer size {:08X}",
+              output_info, size);
 
     IPC::ResponseBuilder rb{ctx, 4};
-    rb.Push(ResultSuccess);
+    rb.Push(result);
     rb.Push<u64>(size);
-
-    LOG_DEBUG(Service_Audio, "buffer_size=0x{:X}", size);
 }
 
 void AudRenU::GetAudioDeviceService(Kernel::HLERequestContext& ctx) {
     IPC::RequestParser rp{ctx};
-    const u64 aruid = rp.Pop<u64>();
 
-    LOG_DEBUG(Service_Audio, "called. aruid={:016X}", aruid);
+    const auto applet_resource_user_id = rp.Pop<u64>();
+
+    LOG_DEBUG(Service_Audio, "called. Applet resource id {}", applet_resource_user_id);
 
-    // Revisionless variant of GetAudioDeviceServiceWithRevisionInfo that
-    // always assumes the initial release revision (REV1).
     IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+
     rb.Push(ResultSuccess);
-    rb.PushIpcInterface<IAudioDevice>(system, buffer_event, Common::MakeMagic('R', 'E', 'V', '1'));
+    rb.PushIpcInterface<IAudioDevice>(system, applet_resource_user_id,
+                                      ::Common::MakeMagic('R', 'E', 'V', '1'), num_audio_devices++);
 }
 
 void AudRenU::OpenAudioRendererForManualExecution(Kernel::HLERequestContext& ctx) {
     LOG_DEBUG(Service_Audio, "called");
-
-    OpenAudioRendererImpl(ctx);
 }
 
 void AudRenU::GetAudioDeviceServiceWithRevisionInfo(Kernel::HLERequestContext& ctx) {
     struct Parameters {
         u32 revision;
-        u64 aruid;
+        u64 applet_resource_user_id;
     };
 
     IPC::RequestParser rp{ctx};
-    const auto [revision, aruid] = rp.PopRaw<Parameters>();
 
-    LOG_DEBUG(Service_Audio, "called. revision={:08X}, aruid={:016X}", revision, aruid);
+    const auto [revision, applet_resource_user_id] = rp.PopRaw<Parameters>();
 
-    IPC::ResponseBuilder rb{ctx, 2, 0, 1};
-    rb.Push(ResultSuccess);
-    rb.PushIpcInterface<IAudioDevice>(system, buffer_event, revision);
-}
+    LOG_DEBUG(Service_Audio, "called. Revision {} Applet resource id {}",
+              AudioCore::GetRevisionNum(revision), applet_resource_user_id);
 
-void AudRenU::OpenAudioRendererImpl(Kernel::HLERequestContext& ctx) {
-    IPC::RequestParser rp{ctx};
-    const auto params = rp.PopRaw<AudioCommon::AudioRendererParameter>();
     IPC::ResponseBuilder rb{ctx, 2, 0, 1};
 
     rb.Push(ResultSuccess);
-    rb.PushIpcInterface<IAudioRenderer>(system, params, audren_instance_count++);
-}
-
-bool IsFeatureSupported(AudioFeatures feature, u32_le revision) {
-    // Byte swap
-    const u32_be version_num = revision - Common::MakeMagic('R', 'E', 'V', '0');
-
-    switch (feature) {
-    case AudioFeatures::AudioUSBDeviceOutput:
-        return version_num >= 4U;
-    case AudioFeatures::Splitter:
-        return version_num >= 2U;
-    case AudioFeatures::PerformanceMetricsVersion2:
-    case AudioFeatures::VariadicCommandBuffer:
-        return version_num >= 5U;
-    default:
-        return false;
-    }
+    rb.PushIpcInterface<IAudioDevice>(system, applet_resource_user_id, revision,
+                                      num_audio_devices++);
 }
 
 } // namespace Service::Audio
diff --git a/src/core/hle/service/audio/audren_u.h b/src/core/hle/service/audio/audren_u.h
index 869d390027..4384a9b3c7 100644
--- a/src/core/hle/service/audio/audren_u.h
+++ b/src/core/hle/service/audio/audren_u.h
@@ -3,6 +3,7 @@
 
 #pragma once
 
+#include "audio_core/audio_render_manager.h"
 #include "core/hle/service/kernel_helpers.h"
 #include "core/hle/service/service.h"
 
@@ -15,6 +16,7 @@ class HLERequestContext;
 }
 
 namespace Service::Audio {
+class IAudioRenderer;
 
 class AudRenU final : public ServiceFramework<AudRenU> {
 public:
@@ -23,28 +25,14 @@ public:
 
 private:
     void OpenAudioRenderer(Kernel::HLERequestContext& ctx);
-    void GetAudioRendererWorkBufferSize(Kernel::HLERequestContext& ctx);
+    void GetWorkBufferSize(Kernel::HLERequestContext& ctx);
     void GetAudioDeviceService(Kernel::HLERequestContext& ctx);
     void OpenAudioRendererForManualExecution(Kernel::HLERequestContext& ctx);
     void GetAudioDeviceServiceWithRevisionInfo(Kernel::HLERequestContext& ctx);
 
-    void OpenAudioRendererImpl(Kernel::HLERequestContext& ctx);
-
     KernelHelpers::ServiceContext service_context;
-
-    std::size_t audren_instance_count = 0;
-    Kernel::KEvent* buffer_event;
+    std::unique_ptr<AudioCore::AudioRenderer::Manager> impl;
+    u32 num_audio_devices{0};
 };
 
-// Describes a particular audio feature that may be supported in a particular revision.
-enum class AudioFeatures : u32 {
-    AudioUSBDeviceOutput,
-    Splitter,
-    PerformanceMetricsVersion2,
-    VariadicCommandBuffer,
-};
-
-// Tests if a particular audio feature is supported with a given audio revision.
-bool IsFeatureSupported(AudioFeatures feature, u32_le revision);
-
 } // namespace Service::Audio
diff --git a/src/core/hle/service/audio/errors.h b/src/core/hle/service/audio/errors.h
index ac6c514af1..d706978cb5 100644
--- a/src/core/hle/service/audio/errors.h
+++ b/src/core/hle/service/audio/errors.h
@@ -7,8 +7,17 @@
 
 namespace Service::Audio {
 
+constexpr Result ERR_INVALID_DEVICE_NAME{ErrorModule::Audio, 1};
 constexpr Result ERR_OPERATION_FAILED{ErrorModule::Audio, 2};
+constexpr Result ERR_INVALID_SAMPLE_RATE{ErrorModule::Audio, 3};
+constexpr Result ERR_INSUFFICIENT_BUFFER_SIZE{ErrorModule::Audio, 4};
+constexpr Result ERR_MAXIMUM_SESSIONS_REACHED{ErrorModule::Audio, 5};
 constexpr Result ERR_BUFFER_COUNT_EXCEEDED{ErrorModule::Audio, 8};
+constexpr Result ERR_INVALID_CHANNEL_COUNT{ErrorModule::Audio, 10};
+constexpr Result ERR_INVALID_UPDATE_DATA{ErrorModule::Audio, 41};
+constexpr Result ERR_POOL_MAPPING_FAILED{ErrorModule::Audio, 42};
 constexpr Result ERR_NOT_SUPPORTED{ErrorModule::Audio, 513};
+constexpr Result ERR_INVALID_PROCESS_HANDLE{ErrorModule::Audio, 1536};
+constexpr Result ERR_INVALID_REVISION{ErrorModule::Audio, 1537};
 
 } // namespace Service::Audio
diff --git a/src/core/hle/service/audio/hwopus.cpp b/src/core/hle/service/audio/hwopus.cpp
index 75da659e55..4f2ed2d52e 100644
--- a/src/core/hle/service/audio/hwopus.cpp
+++ b/src/core/hle/service/audio/hwopus.cpp
@@ -298,7 +298,7 @@ void HwOpus::OpenHardwareOpusDecoderEx(Kernel::HLERequestContext& ctx) {
     const auto sample_rate = rp.Pop<u32>();
     const auto channel_count = rp.Pop<u32>();
 
-    LOG_CRITICAL(Audio, "called sample_rate={}, channel_count={}", sample_rate, channel_count);
+    LOG_DEBUG(Audio, "called sample_rate={}, channel_count={}", sample_rate, channel_count);
 
     ASSERT_MSG(sample_rate == 48000 || sample_rate == 24000 || sample_rate == 16000 ||
                    sample_rate == 12000 || sample_rate == 8000,
diff --git a/src/core/memory.cpp b/src/core/memory.cpp
index 584808d50d..635449fce0 100644
--- a/src/core/memory.cpp
+++ b/src/core/memory.cpp
@@ -511,7 +511,7 @@ struct Memory::Impl {
 
     [[nodiscard]] u8* GetPointerImpl(VAddr vaddr, auto on_unmapped, auto on_rasterizer) const {
         // AARCH64 masks the upper 16 bit of all memory accesses
-        vaddr &= 0xffffffffffffLL;
+        vaddr &= 0xffffffffffffULL;
 
         if (vaddr >= 1uLL << current_page_table->GetAddressSpaceBits()) {
             on_unmapped();
@@ -776,6 +776,10 @@ void Memory::CopyBlock(const Kernel::KProcess& process, VAddr dest_addr, VAddr s
     impl->CopyBlock(process, dest_addr, src_addr, size);
 }
 
+void Memory::ZeroBlock(const Kernel::KProcess& process, VAddr dest_addr, const std::size_t size) {
+    impl->ZeroBlock(process, dest_addr, size);
+}
+
 void Memory::RasterizerMarkRegionCached(VAddr vaddr, u64 size, bool cached) {
     impl->RasterizerMarkRegionCached(vaddr, size, cached);
 }
diff --git a/src/core/memory.h b/src/core/memory.h
index f22c0a2d87..780c45385f 100644
--- a/src/core/memory.h
+++ b/src/core/memory.h
@@ -436,6 +436,19 @@ public:
     void CopyBlock(const Kernel::KProcess& process, VAddr dest_addr, VAddr src_addr,
                    std::size_t size);
 
+    /**
+     * Zeros a range of bytes within the current process' address space at the specified
+     * virtual address.
+     *
+     * @param process   The process that will have data zeroed within its address space.
+     * @param dest_addr The destination virtual address to zero the data from.
+     * @param size      The size of the range to zero out, in bytes.
+     *
+     * @post The range [dest_addr, size) within the process' address space contains the
+     *       value 0.
+     */
+    void ZeroBlock(const Kernel::KProcess& process, VAddr dest_addr, std::size_t size);
+
     /**
      * Marks each page within the specified address range as cached or uncached.
      *
diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp
index 0a61839da9..2840bc5eb1 100644
--- a/src/yuzu/configuration/config.cpp
+++ b/src/yuzu/configuration/config.cpp
@@ -372,8 +372,9 @@ void Config::ReadAudioValues() {
     qt_config->beginGroup(QStringLiteral("Audio"));
 
     if (global) {
-        ReadBasicSetting(Settings::values.audio_device_id);
         ReadBasicSetting(Settings::values.sink_id);
+        ReadBasicSetting(Settings::values.audio_output_device_id);
+        ReadBasicSetting(Settings::values.audio_input_device_id);
     }
     ReadGlobalSetting(Settings::values.volume);
 
@@ -1027,7 +1028,8 @@ void Config::SaveAudioValues() {
 
     if (global) {
         WriteBasicSetting(Settings::values.sink_id);
-        WriteBasicSetting(Settings::values.audio_device_id);
+        WriteBasicSetting(Settings::values.audio_output_device_id);
+        WriteBasicSetting(Settings::values.audio_input_device_id);
     }
     WriteGlobalSetting(Settings::values.volume);
 
diff --git a/src/yuzu/configuration/configure_audio.cpp b/src/yuzu/configuration/configure_audio.cpp
index 512bdfc229..19b8b15ef5 100644
--- a/src/yuzu/configuration/configure_audio.cpp
+++ b/src/yuzu/configuration/configure_audio.cpp
@@ -3,8 +3,8 @@
 
 #include <memory>
 
-#include "audio_core/sink.h"
-#include "audio_core/sink_details.h"
+#include "audio_core/sink/sink.h"
+#include "audio_core/sink/sink_details.h"
 #include "common/settings.h"
 #include "core/core.h"
 #include "ui_configure_audio.h"
@@ -15,11 +15,11 @@ ConfigureAudio::ConfigureAudio(const Core::System& system_, QWidget* parent)
     : QWidget(parent), ui(std::make_unique<Ui::ConfigureAudio>()), system{system_} {
     ui->setupUi(this);
 
-    InitializeAudioOutputSinkComboBox();
+    InitializeAudioSinkComboBox();
 
     connect(ui->volume_slider, &QSlider::valueChanged, this,
             &ConfigureAudio::SetVolumeIndicatorText);
-    connect(ui->output_sink_combo_box, qOverload<int>(&QComboBox::currentIndexChanged), this,
+    connect(ui->sink_combo_box, qOverload<int>(&QComboBox::currentIndexChanged), this,
             &ConfigureAudio::UpdateAudioDevices);
 
     ui->volume_label->setVisible(Settings::IsConfiguringGlobal());
@@ -30,8 +30,9 @@ ConfigureAudio::ConfigureAudio(const Core::System& system_, QWidget* parent)
     SetConfiguration();
 
     const bool is_powered_on = system_.IsPoweredOn();
-    ui->output_sink_combo_box->setEnabled(!is_powered_on);
-    ui->audio_device_combo_box->setEnabled(!is_powered_on);
+    ui->sink_combo_box->setEnabled(!is_powered_on);
+    ui->output_combo_box->setEnabled(!is_powered_on);
+    ui->input_combo_box->setEnabled(!is_powered_on);
 }
 
 ConfigureAudio::~ConfigureAudio() = default;
@@ -40,9 +41,9 @@ void ConfigureAudio::SetConfiguration() {
     SetOutputSinkFromSinkID();
 
     // The device list cannot be pre-populated (nor listed) until the output sink is known.
-    UpdateAudioDevices(ui->output_sink_combo_box->currentIndex());
+    UpdateAudioDevices(ui->sink_combo_box->currentIndex());
 
-    SetAudioDeviceFromDeviceID();
+    SetAudioDevicesFromDeviceID();
 
     const auto volume_value = static_cast<int>(Settings::values.volume.GetValue());
     ui->volume_slider->setValue(volume_value);
@@ -62,32 +63,45 @@ void ConfigureAudio::SetConfiguration() {
 }
 
 void ConfigureAudio::SetOutputSinkFromSinkID() {
-    [[maybe_unused]] const QSignalBlocker blocker(ui->output_sink_combo_box);
+    [[maybe_unused]] const QSignalBlocker blocker(ui->sink_combo_box);
 
     int new_sink_index = 0;
     const QString sink_id = QString::fromStdString(Settings::values.sink_id.GetValue());
-    for (int index = 0; index < ui->output_sink_combo_box->count(); index++) {
-        if (ui->output_sink_combo_box->itemText(index) == sink_id) {
+    for (int index = 0; index < ui->sink_combo_box->count(); index++) {
+        if (ui->sink_combo_box->itemText(index) == sink_id) {
             new_sink_index = index;
             break;
         }
     }
 
-    ui->output_sink_combo_box->setCurrentIndex(new_sink_index);
+    ui->sink_combo_box->setCurrentIndex(new_sink_index);
 }
 
-void ConfigureAudio::SetAudioDeviceFromDeviceID() {
+void ConfigureAudio::SetAudioDevicesFromDeviceID() {
     int new_device_index = -1;
 
-    const QString device_id = QString::fromStdString(Settings::values.audio_device_id.GetValue());
-    for (int index = 0; index < ui->audio_device_combo_box->count(); index++) {
-        if (ui->audio_device_combo_box->itemText(index) == device_id) {
+    const QString output_device_id =
+        QString::fromStdString(Settings::values.audio_output_device_id.GetValue());
+    for (int index = 0; index < ui->output_combo_box->count(); index++) {
+        if (ui->output_combo_box->itemText(index) == output_device_id) {
             new_device_index = index;
             break;
         }
     }
 
-    ui->audio_device_combo_box->setCurrentIndex(new_device_index);
+    ui->output_combo_box->setCurrentIndex(new_device_index);
+
+    new_device_index = -1;
+    const QString input_device_id =
+        QString::fromStdString(Settings::values.audio_input_device_id.GetValue());
+    for (int index = 0; index < ui->input_combo_box->count(); index++) {
+        if (ui->input_combo_box->itemText(index) == input_device_id) {
+            new_device_index = index;
+            break;
+        }
+    }
+
+    ui->input_combo_box->setCurrentIndex(new_device_index);
 }
 
 void ConfigureAudio::SetVolumeIndicatorText(int percentage) {
@@ -95,14 +109,13 @@ void ConfigureAudio::SetVolumeIndicatorText(int percentage) {
 }
 
 void ConfigureAudio::ApplyConfiguration() {
-
     if (Settings::IsConfiguringGlobal()) {
         Settings::values.sink_id =
-            ui->output_sink_combo_box->itemText(ui->output_sink_combo_box->currentIndex())
-                .toStdString();
-        Settings::values.audio_device_id.SetValue(
-            ui->audio_device_combo_box->itemText(ui->audio_device_combo_box->currentIndex())
-                .toStdString());
+            ui->sink_combo_box->itemText(ui->sink_combo_box->currentIndex()).toStdString();
+        Settings::values.audio_output_device_id.SetValue(
+            ui->output_combo_box->itemText(ui->output_combo_box->currentIndex()).toStdString());
+        Settings::values.audio_input_device_id.SetValue(
+            ui->input_combo_box->itemText(ui->input_combo_box->currentIndex()).toStdString());
 
         // Guard if during game and set to game-specific value
         if (Settings::values.volume.UsingGlobal()) {
@@ -129,21 +142,27 @@ void ConfigureAudio::changeEvent(QEvent* event) {
 }
 
 void ConfigureAudio::UpdateAudioDevices(int sink_index) {
-    ui->audio_device_combo_box->clear();
-    ui->audio_device_combo_box->addItem(QString::fromUtf8(AudioCore::auto_device_name));
+    ui->output_combo_box->clear();
+    ui->output_combo_box->addItem(QString::fromUtf8(AudioCore::Sink::auto_device_name));
 
-    const std::string sink_id = ui->output_sink_combo_box->itemText(sink_index).toStdString();
-    for (const auto& device : AudioCore::GetDeviceListForSink(sink_id)) {
-        ui->audio_device_combo_box->addItem(QString::fromStdString(device));
+    const std::string sink_id = ui->sink_combo_box->itemText(sink_index).toStdString();
+    for (const auto& device : AudioCore::Sink::GetDeviceListForSink(sink_id, false)) {
+        ui->output_combo_box->addItem(QString::fromStdString(device));
+    }
+
+    ui->input_combo_box->clear();
+    ui->input_combo_box->addItem(QString::fromUtf8(AudioCore::Sink::auto_device_name));
+    for (const auto& device : AudioCore::Sink::GetDeviceListForSink(sink_id, true)) {
+        ui->input_combo_box->addItem(QString::fromStdString(device));
     }
 }
 
-void ConfigureAudio::InitializeAudioOutputSinkComboBox() {
-    ui->output_sink_combo_box->clear();
-    ui->output_sink_combo_box->addItem(QString::fromUtf8(AudioCore::auto_device_name));
+void ConfigureAudio::InitializeAudioSinkComboBox() {
+    ui->sink_combo_box->clear();
+    ui->sink_combo_box->addItem(QString::fromUtf8(AudioCore::Sink::auto_device_name));
 
-    for (const char* id : AudioCore::GetSinkIDs()) {
-        ui->output_sink_combo_box->addItem(QString::fromUtf8(id));
+    for (const char* id : AudioCore::Sink::GetSinkIDs()) {
+        ui->sink_combo_box->addItem(QString::fromUtf8(id));
     }
 }
 
@@ -164,8 +183,10 @@ void ConfigureAudio::SetupPerGameUI() {
         ConfigurationShared::SetHighlight(ui->volume_layout, index == 1);
     });
 
-    ui->output_sink_combo_box->setVisible(false);
-    ui->output_sink_label->setVisible(false);
-    ui->audio_device_combo_box->setVisible(false);
-    ui->audio_device_label->setVisible(false);
+    ui->sink_combo_box->setVisible(false);
+    ui->sink_label->setVisible(false);
+    ui->output_combo_box->setVisible(false);
+    ui->output_label->setVisible(false);
+    ui->input_combo_box->setVisible(false);
+    ui->input_label->setVisible(false);
 }
diff --git a/src/yuzu/configuration/configure_audio.h b/src/yuzu/configuration/configure_audio.h
index 08c278eeb5..0d03aae1df 100644
--- a/src/yuzu/configuration/configure_audio.h
+++ b/src/yuzu/configuration/configure_audio.h
@@ -31,14 +31,14 @@ public:
 private:
     void changeEvent(QEvent* event) override;
 
-    void InitializeAudioOutputSinkComboBox();
+    void InitializeAudioSinkComboBox();
 
     void RetranslateUI();
 
     void UpdateAudioDevices(int sink_index);
 
     void SetOutputSinkFromSinkID();
-    void SetAudioDeviceFromDeviceID();
+    void SetAudioDevicesFromDeviceID();
     void SetVolumeIndicatorText(int percentage);
 
     void SetupPerGameUI();
diff --git a/src/yuzu/configuration/configure_audio.ui b/src/yuzu/configuration/configure_audio.ui
index d1ac8ad02d..a5bcee4152 100644
--- a/src/yuzu/configuration/configure_audio.ui
+++ b/src/yuzu/configuration/configure_audio.ui
@@ -21,30 +21,44 @@
      </property>
      <layout class="QVBoxLayout">
       <item>
-       <layout class="QHBoxLayout" name="_3">
+       <layout class="QHBoxLayout" name="engine_layout">
         <item>
-         <widget class="QLabel" name="output_sink_label">
+         <widget class="QLabel" name="sink_label">
           <property name="text">
            <string>Output Engine:</string>
           </property>
          </widget>
         </item>
         <item>
-         <widget class="QComboBox" name="output_sink_combo_box"/>
+         <widget class="QComboBox" name="sink_combo_box"/>
         </item>
        </layout>
       </item>
       <item>
-       <layout class="QHBoxLayout" name="_2">
+       <layout class="QHBoxLayout" name="output_layout">
         <item>
-         <widget class="QLabel" name="audio_device_label">
+         <widget class="QLabel" name="output_label">
           <property name="text">
-           <string>Audio Device:</string>
+           <string>Output Device</string>
           </property>
          </widget>
         </item>
         <item>
-         <widget class="QComboBox" name="audio_device_combo_box"/>
+         <widget class="QComboBox" name="output_combo_box"/>
+        </item>
+       </layout>
+      </item>
+      <item>
+       <layout class="QHBoxLayout" name="input_layout">
+        <item>
+         <widget class="QLabel" name="input_label">
+          <property name="text">
+           <string>Input Device</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QComboBox" name="input_combo_box"/>
         </item>
        </layout>
       </item>
diff --git a/src/yuzu/configuration/configure_debug.cpp b/src/yuzu/configuration/configure_debug.cpp
index 343d2aee11..84808f678a 100644
--- a/src/yuzu/configuration/configure_debug.cpp
+++ b/src/yuzu/configuration/configure_debug.cpp
@@ -44,6 +44,7 @@ void ConfigureDebug::SetConfiguration() {
     ui->fs_access_log->setEnabled(runtime_lock);
     ui->fs_access_log->setChecked(Settings::values.enable_fs_access_log.GetValue());
     ui->reporting_services->setChecked(Settings::values.reporting_services.GetValue());
+    ui->dump_audio_commands->setChecked(Settings::values.dump_audio_commands.GetValue());
     ui->quest_flag->setChecked(Settings::values.quest_flag.GetValue());
     ui->use_debug_asserts->setChecked(Settings::values.use_debug_asserts.GetValue());
     ui->use_auto_stub->setChecked(Settings::values.use_auto_stub.GetValue());
@@ -83,6 +84,7 @@ void ConfigureDebug::ApplyConfiguration() {
     Settings::values.program_args = ui->homebrew_args_edit->text().toStdString();
     Settings::values.enable_fs_access_log = ui->fs_access_log->isChecked();
     Settings::values.reporting_services = ui->reporting_services->isChecked();
+    Settings::values.dump_audio_commands = ui->dump_audio_commands->isChecked();
     Settings::values.quest_flag = ui->quest_flag->isChecked();
     Settings::values.use_debug_asserts = ui->use_debug_asserts->isChecked();
     Settings::values.use_auto_stub = ui->use_auto_stub->isChecked();
diff --git a/src/yuzu/configuration/configure_debug.ui b/src/yuzu/configuration/configure_debug.ui
index 1152fa6c6b..4c16274fc6 100644
--- a/src/yuzu/configuration/configure_debug.ui
+++ b/src/yuzu/configuration/configure_debug.ui
@@ -235,6 +235,16 @@
        </widget>
       </item>
       <item row="1" column="0">
+       <widget class="QCheckBox" name="dump_audio_commands">
+        <property name="text">
+         <string>Dump Audio Commands To Console**</string>
+        </property>
+        <property name="toolTip">
+         <string>Enable this to output the latest generated audio command list to the console. Only affects games using the audio renderer.</string>
+        </property>
+       </widget>
+      </item>
+      <item row="2" column="0">
        <widget class="QCheckBox" name="reporting_services">
         <property name="text">
          <string>Enable Verbose Reporting Services**</string>
@@ -325,6 +335,7 @@
   <tabstop>disable_loop_safety_checks</tabstop>
   <tabstop>fs_access_log</tabstop>
   <tabstop>reporting_services</tabstop>
+  <tabstop>dump_audio_commands</tabstop>
   <tabstop>quest_flag</tabstop>
   <tabstop>enable_cpu_debugging</tabstop>
   <tabstop>use_debug_asserts</tabstop>
diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp
index e60d840548..a120f26623 100644
--- a/src/yuzu/main.cpp
+++ b/src/yuzu/main.cpp
@@ -1498,6 +1498,8 @@ void GMainWindow::BootGame(const QString& filename, u64 program_id, std::size_t
     if (!LoadROM(filename, program_id, program_index))
         return;
 
+    system->SetShuttingDown(false);
+
     // Create and start the emulation thread
     emu_thread = std::make_unique<EmuThread>(*system);
     emit EmulationStarting(emu_thread.get());
@@ -1588,6 +1590,7 @@ void GMainWindow::ShutdownGame() {
 
     AllowOSSleep();
 
+    system->SetShuttingDown(true);
     system->DetachDebugger();
     discord_rpc->Pause();
     emu_thread->RequestStop();
diff --git a/src/yuzu_cmd/config.cpp b/src/yuzu_cmd/config.cpp
index 5576fb7954..ad7f9d2393 100644
--- a/src/yuzu_cmd/config.cpp
+++ b/src/yuzu_cmd/config.cpp
@@ -322,7 +322,7 @@ void Config::ReadValues() {
 
     // Audio
     ReadSetting("Audio", Settings::values.sink_id);
-    ReadSetting("Audio", Settings::values.audio_device_id);
+    ReadSetting("Audio", Settings::values.audio_output_device_id);
     ReadSetting("Audio", Settings::values.volume);
 
     // Miscellaneous