From 67e2d5c28b8423c4f3f1d5b00f87325684158a6f Mon Sep 17 00:00:00 2001
From: Kelebek1 <eeeedddccc@hotmail.co.uk>
Date: Thu, 31 Aug 2023 15:09:15 +0100
Subject: [PATCH] Reimplement HardwareOpus

---
 src/audio_core/CMakeLists.txt                 |  16 +-
 src/audio_core/adsp/adsp.cpp                  |  13 +-
 src/audio_core/adsp/adsp.h                    |   3 +
 .../apps/audio_renderer/audio_renderer.cpp    |  48 +-
 .../adsp/apps/audio_renderer/audio_renderer.h |  45 +-
 .../adsp/apps/opus/opus_decode_object.cpp     | 107 +++
 .../adsp/apps/opus/opus_decode_object.h       |  38 +
 .../adsp/apps/opus/opus_decoder.cpp           | 269 +++++++
 src/audio_core/adsp/apps/opus/opus_decoder.h  |  92 +++
 .../opus/opus_multistream_decode_object.cpp   | 111 +++
 .../opus/opus_multistream_decode_object.h     |  39 +
 src/audio_core/adsp/apps/opus/shared_memory.h |  17 +
 src/audio_core/adsp/mailbox.h                 |  27 +-
 src/audio_core/opus/decoder.cpp               | 179 +++++
 src/audio_core/opus/decoder.h                 |  53 ++
 src/audio_core/opus/decoder_manager.cpp       | 102 +++
 src/audio_core/opus/decoder_manager.h         |  38 +
 src/audio_core/opus/hardware_opus.cpp         | 241 ++++++
 src/audio_core/opus/hardware_opus.h           |  45 ++
 src/audio_core/opus/parameters.h              |  54 ++
 src/common/CMakeLists.txt                     |   5 +-
 src/common/bounded_threadsafe_queue.h         |   4 +-
 src/core/CMakeLists.txt                       |   2 +-
 src/core/hle/result.h                         |   2 +-
 src/core/hle/service/audio/errors.h           |  12 +
 src/core/hle/service/audio/hwopus.cpp         | 754 ++++++++++--------
 src/core/hle/service/audio/hwopus.h           |  25 +-
 27 files changed, 1914 insertions(+), 427 deletions(-)
 create mode 100644 src/audio_core/adsp/apps/opus/opus_decode_object.cpp
 create mode 100644 src/audio_core/adsp/apps/opus/opus_decode_object.h
 create mode 100644 src/audio_core/adsp/apps/opus/opus_decoder.cpp
 create mode 100644 src/audio_core/adsp/apps/opus/opus_decoder.h
 create mode 100644 src/audio_core/adsp/apps/opus/opus_multistream_decode_object.cpp
 create mode 100644 src/audio_core/adsp/apps/opus/opus_multistream_decode_object.h
 create mode 100644 src/audio_core/adsp/apps/opus/shared_memory.h
 create mode 100644 src/audio_core/opus/decoder.cpp
 create mode 100644 src/audio_core/opus/decoder.h
 create mode 100644 src/audio_core/opus/decoder_manager.cpp
 create mode 100644 src/audio_core/opus/decoder_manager.h
 create mode 100644 src/audio_core/opus/hardware_opus.cpp
 create mode 100644 src/audio_core/opus/hardware_opus.h
 create mode 100644 src/audio_core/opus/parameters.h

diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt
index 67dfe0290d..400988c5fb 100644
--- a/src/audio_core/CMakeLists.txt
+++ b/src/audio_core/CMakeLists.txt
@@ -10,6 +10,13 @@ add_library(audio_core STATIC
     adsp/apps/audio_renderer/command_buffer.h
     adsp/apps/audio_renderer/command_list_processor.cpp
     adsp/apps/audio_renderer/command_list_processor.h
+    adsp/apps/opus/opus_decoder.cpp
+    adsp/apps/opus/opus_decoder.h
+    adsp/apps/opus/opus_decode_object.cpp
+    adsp/apps/opus/opus_decode_object.h
+    adsp/apps/opus/opus_multistream_decode_object.cpp
+    adsp/apps/opus/opus_multistream_decode_object.h
+    adsp/apps/opus/shared_memory.h
     audio_core.cpp
     audio_core.h
     audio_event.h
@@ -35,6 +42,13 @@ add_library(audio_core STATIC
     in/audio_in.h
     in/audio_in_system.cpp
     in/audio_in_system.h
+    opus/hardware_opus.cpp
+    opus/hardware_opus.h
+    opus/decoder_manager.cpp
+    opus/decoder_manager.h
+    opus/decoder.cpp
+    opus/decoder.h
+    opus/parameters.h
     out/audio_out.cpp
     out/audio_out.h
     out/audio_out_system.cpp
@@ -214,7 +228,7 @@ else()
     )
 endif()
 
-target_link_libraries(audio_core PUBLIC common core)
+target_link_libraries(audio_core PUBLIC common core Opus::opus)
 if (ARCHITECTURE_x86_64 OR ARCHITECTURE_arm64)
     target_link_libraries(audio_core PRIVATE dynarmic::dynarmic)
 endif()
diff --git a/src/audio_core/adsp/adsp.cpp b/src/audio_core/adsp/adsp.cpp
index 0580990f5b..6c53c98fd5 100644
--- a/src/audio_core/adsp/adsp.cpp
+++ b/src/audio_core/adsp/adsp.cpp
@@ -7,12 +7,21 @@
 namespace AudioCore::ADSP {
 
 ADSP::ADSP(Core::System& system, Sink::Sink& sink) {
-    audio_renderer =
-        std::make_unique<AudioRenderer::AudioRenderer>(system, system.ApplicationMemory(), sink);
+    audio_renderer = std::make_unique<AudioRenderer::AudioRenderer>(system, sink);
+    opus_decoder = std::make_unique<OpusDecoder::OpusDecoder>(system);
+    opus_decoder->Send(Direction::DSP, OpusDecoder::Message::Start);
+    if (opus_decoder->Receive(Direction::Host) != OpusDecoder::Message::StartOK) {
+        LOG_ERROR(Service_Audio, "OpusDeocder failed to initialize.");
+        return;
+    }
 }
 
 AudioRenderer::AudioRenderer& ADSP::AudioRenderer() {
     return *audio_renderer.get();
 }
 
+OpusDecoder::OpusDecoder& ADSP::OpusDecoder() {
+    return *opus_decoder.get();
+}
+
 } // namespace AudioCore::ADSP
diff --git a/src/audio_core/adsp/adsp.h b/src/audio_core/adsp/adsp.h
index bd5bcc63ba..a0c24a16a2 100644
--- a/src/audio_core/adsp/adsp.h
+++ b/src/audio_core/adsp/adsp.h
@@ -4,6 +4,7 @@
 #pragma once
 
 #include "audio_core/adsp/apps/audio_renderer/audio_renderer.h"
+#include "audio_core/adsp/apps/opus/opus_decoder.h"
 #include "common/common_types.h"
 
 namespace Core {
@@ -40,10 +41,12 @@ public:
     ~ADSP() = default;
 
     AudioRenderer::AudioRenderer& AudioRenderer();
+    OpusDecoder::OpusDecoder& OpusDecoder();
 
 private:
     /// AudioRenderer app
     std::unique_ptr<AudioRenderer::AudioRenderer> audio_renderer{};
+    std::unique_ptr<OpusDecoder::OpusDecoder> opus_decoder{};
 };
 
 } // namespace ADSP
diff --git a/src/audio_core/adsp/apps/audio_renderer/audio_renderer.cpp b/src/audio_core/adsp/apps/audio_renderer/audio_renderer.cpp
index 2e549bc6f5..972d5e45bc 100644
--- a/src/audio_core/adsp/apps/audio_renderer/audio_renderer.cpp
+++ b/src/audio_core/adsp/apps/audio_renderer/audio_renderer.cpp
@@ -14,13 +14,12 @@
 #include "core/core.h"
 #include "core/core_timing.h"
 
-MICROPROFILE_DEFINE(Audio_Renderer, "Audio", "DSP", MP_RGB(60, 19, 97));
+MICROPROFILE_DEFINE(Audio_Renderer, "Audio", "DSP_AudioRenderer", MP_RGB(60, 19, 97));
 
 namespace AudioCore::ADSP::AudioRenderer {
 
-AudioRenderer::AudioRenderer(Core::System& system_, Core::Memory::Memory& memory_,
-                             Sink::Sink& sink_)
-    : system{system_}, memory{memory_}, sink{sink_} {}
+AudioRenderer::AudioRenderer(Core::System& system_, Sink::Sink& sink_)
+    : system{system_}, sink{sink_} {}
 
 AudioRenderer::~AudioRenderer() {
     Stop();
@@ -33,8 +32,8 @@ void AudioRenderer::Start() {
 
     main_thread = std::jthread([this](std::stop_token stop_token) { Main(stop_token); });
 
-    mailbox.Send(Direction::DSP, {Message::InitializeOK, {}});
-    if (mailbox.Receive(Direction::Host).msg != Message::InitializeOK) {
+    mailbox.Send(Direction::DSP, Message::InitializeOK);
+    if (mailbox.Receive(Direction::Host) != Message::InitializeOK) {
         LOG_ERROR(Service_Audio, "Host Audio Renderer -- Failed to receive shutdown "
                                  "message response from ADSP!");
         return;
@@ -47,8 +46,8 @@ void AudioRenderer::Stop() {
         return;
     }
 
-    mailbox.Send(Direction::DSP, {Message::Shutdown, {}});
-    if (mailbox.Receive(Direction::Host).msg != Message::Shutdown) {
+    mailbox.Send(Direction::DSP, Message::Shutdown);
+    if (mailbox.Receive(Direction::Host) != Message::Shutdown) {
         LOG_ERROR(Service_Audio, "Host Audio Renderer -- Failed to receive shutdown "
                                  "message response from ADSP!");
     }
@@ -67,25 +66,25 @@ void AudioRenderer::Stop() {
 
 void AudioRenderer::Signal() {
     signalled_tick = system.CoreTiming().GetGlobalTimeNs().count();
-    Send(Direction::DSP, {Message::Render, {}});
+    Send(Direction::DSP, Message::Render);
 }
 
 void AudioRenderer::Wait() {
-    auto received = Receive(Direction::Host);
-    if (received.msg != Message::RenderResponse) {
+    auto msg = Receive(Direction::Host);
+    if (msg != Message::RenderResponse) {
         LOG_ERROR(Service_Audio,
                   "Did not receive the expected render response from the AudioRenderer! Expected "
                   "{}, got {}",
-                  Message::RenderResponse, received.msg);
+                  Message::RenderResponse, msg);
     }
 }
 
-void AudioRenderer::Send(Direction dir, MailboxMessage message) {
+void AudioRenderer::Send(Direction dir, u32 message) {
     mailbox.Send(dir, std::move(message));
 }
 
-MailboxMessage AudioRenderer::Receive(Direction dir, bool block) {
-    return mailbox.Receive(dir, block);
+u32 AudioRenderer::Receive(Direction dir) {
+    return mailbox.Receive(dir);
 }
 
 void AudioRenderer::SetCommandBuffer(s32 session_id, CpuAddr buffer, u64 size, u64 time_limit,
@@ -120,7 +119,7 @@ void AudioRenderer::CreateSinkStreams() {
 }
 
 void AudioRenderer::Main(std::stop_token stop_token) {
-    static constexpr char name[]{"AudioRenderer"};
+    static constexpr char name[]{"DSP_AudioRenderer_Main"};
     MicroProfileOnThreadCreate(name);
     Common::SetCurrentThreadName(name);
     Common::SetCurrentThreadPriority(Common::ThreadPriority::High);
@@ -128,28 +127,28 @@ void AudioRenderer::Main(std::stop_token stop_token) {
     // TODO: Create buffer map/unmap thread + mailbox
     // TODO: Create gMix devices, initialize them here
 
-    if (mailbox.Receive(Direction::DSP).msg != Message::InitializeOK) {
+    if (mailbox.Receive(Direction::DSP) != Message::InitializeOK) {
         LOG_ERROR(Service_Audio,
                   "ADSP Audio Renderer -- Failed to receive initialize message from host!");
         return;
     }
 
-    mailbox.Send(Direction::Host, {Message::InitializeOK, {}});
+    mailbox.Send(Direction::Host, Message::InitializeOK);
 
     // 0.12 seconds (2,304,000 / 19,200,000)
     constexpr u64 max_process_time{2'304'000ULL};
 
     while (!stop_token.stop_requested()) {
-        auto received{mailbox.Receive(Direction::DSP)};
-        switch (received.msg) {
+        auto msg{mailbox.Receive(Direction::DSP)};
+        switch (msg) {
         case Message::Shutdown:
-            mailbox.Send(Direction::Host, {Message::Shutdown, {}});
+            mailbox.Send(Direction::Host, Message::Shutdown);
             return;
 
         case Message::Render: {
             if (system.IsShuttingDown()) [[unlikely]] {
                 std::this_thread::sleep_for(std::chrono::milliseconds(5));
-                mailbox.Send(Direction::Host, {Message::RenderResponse, {}});
+                mailbox.Send(Direction::Host, Message::RenderResponse);
                 continue;
             }
             std::array<bool, MaxRendererSessions> buffers_reset{};
@@ -205,13 +204,12 @@ void AudioRenderer::Main(std::stop_token stop_token) {
                 }
             }
 
-            mailbox.Send(Direction::Host, {Message::RenderResponse, {}});
+            mailbox.Send(Direction::Host, Message::RenderResponse);
         } break;
 
         default:
             LOG_WARNING(Service_Audio,
-                        "ADSP AudioRenderer received an invalid message, msg={:02X}!",
-                        received.msg);
+                        "ADSP AudioRenderer received an invalid message, msg={:02X}!", msg);
             break;
         }
     }
diff --git a/src/audio_core/adsp/apps/audio_renderer/audio_renderer.h b/src/audio_core/adsp/apps/audio_renderer/audio_renderer.h
index 3f5b7dca2a..85874d88ac 100644
--- a/src/audio_core/adsp/apps/audio_renderer/audio_renderer.h
+++ b/src/audio_core/adsp/apps/audio_renderer/audio_renderer.h
@@ -17,13 +17,6 @@
 
 namespace Core {
 class System;
-namespace Timing {
-struct EventType;
-}
-namespace Memory {
-class Memory;
-}
-class System;
 } // namespace Core
 
 namespace AudioCore {
@@ -34,19 +27,19 @@ class Sink;
 namespace ADSP::AudioRenderer {
 
 enum Message : u32 {
-    Invalid = 0x00,
-    MapUnmap_Map = 0x01,
-    MapUnmap_MapResponse = 0x02,
-    MapUnmap_Unmap = 0x03,
-    MapUnmap_UnmapResponse = 0x04,
-    MapUnmap_InvalidateCache = 0x05,
-    MapUnmap_InvalidateCacheResponse = 0x06,
-    MapUnmap_Shutdown = 0x07,
-    MapUnmap_ShutdownResponse = 0x08,
-    InitializeOK = 0x16,
-    RenderResponse = 0x20,
-    Render = 0x2A,
-    Shutdown = 0x34,
+    Invalid = 0,
+    MapUnmap_Map = 1,
+    MapUnmap_MapResponse = 2,
+    MapUnmap_Unmap = 3,
+    MapUnmap_UnmapResponse = 4,
+    MapUnmap_InvalidateCache = 5,
+    MapUnmap_InvalidateCacheResponse = 6,
+    MapUnmap_Shutdown = 7,
+    MapUnmap_ShutdownResponse = 8,
+    InitializeOK = 22,
+    RenderResponse = 32,
+    Render = 42,
+    Shutdown = 52,
 };
 
 /**
@@ -54,7 +47,7 @@ enum Message : u32 {
  */
 class AudioRenderer {
 public:
-    explicit AudioRenderer(Core::System& system, Core::Memory::Memory& memory, Sink::Sink& sink);
+    explicit AudioRenderer(Core::System& system, Sink::Sink& sink);
     ~AudioRenderer();
 
     /**
@@ -72,8 +65,8 @@ public:
     void Signal();
     void Wait();
 
-    void Send(Direction dir, MailboxMessage message);
-    MailboxMessage Receive(Direction dir, bool block = true);
+    void Send(Direction dir, u32 message);
+    u32 Receive(Direction dir);
 
     void SetCommandBuffer(s32 session_id, CpuAddr buffer, u64 size, u64 time_limit,
                           u64 applet_resource_user_id, bool reset) noexcept;
@@ -94,9 +87,7 @@ private:
 
     /// Core system
     Core::System& system;
-    /// Memory
-    Core::Memory::Memory& memory;
-    /// The output sink the AudioRenderer will use
+    /// The output sink the AudioRenderer will send samples to
     Sink::Sink& sink;
     /// The active mailbox
     Mailbox mailbox;
@@ -104,11 +95,13 @@ private:
     std::jthread main_thread{};
     /// The current state
     std::atomic<bool> running{};
+    /// Shared memory of input command buffers, set by host, read by DSP
     std::array<CommandBuffer, MaxRendererSessions> command_buffers{};
     /// The command lists to process
     std::array<CommandListProcessor, MaxRendererSessions> command_list_processors{};
     /// The streams which will receive the processed samples
     std::array<Sink::SinkStream*, MaxRendererSessions> streams{};
+    /// CPU Tick when the DSP was signalled to process, uses time rather than tick
     u64 signalled_tick{0};
 };
 
diff --git a/src/audio_core/adsp/apps/opus/opus_decode_object.cpp b/src/audio_core/adsp/apps/opus/opus_decode_object.cpp
new file mode 100644
index 0000000000..2c16d37692
--- /dev/null
+++ b/src/audio_core/adsp/apps/opus/opus_decode_object.cpp
@@ -0,0 +1,107 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/adsp/apps/opus/opus_decode_object.h"
+#include "common/assert.h"
+
+namespace AudioCore::ADSP::OpusDecoder {
+namespace {
+bool IsValidChannelCount(u32 channel_count) {
+    return channel_count == 1 || channel_count == 2;
+}
+} // namespace
+
+u32 OpusDecodeObject::GetWorkBufferSize(u32 channel_count) {
+    if (!IsValidChannelCount(channel_count)) {
+        return 0;
+    }
+    return static_cast<u32>(sizeof(OpusDecodeObject)) + opus_decoder_get_size(channel_count);
+}
+
+OpusDecodeObject& OpusDecodeObject::Initialize(u64 buffer, u64 buffer2) {
+    auto* new_decoder = reinterpret_cast<OpusDecodeObject*>(buffer);
+    auto* comparison = reinterpret_cast<OpusDecodeObject*>(buffer2);
+
+    if (new_decoder->magic == DecodeObjectMagic) {
+        if (!new_decoder->initialized ||
+            (new_decoder->initialized && new_decoder->self == comparison)) {
+            new_decoder->state_valid = true;
+        }
+    } else {
+        new_decoder->initialized = false;
+        new_decoder->state_valid = true;
+    }
+    return *new_decoder;
+}
+
+s32 OpusDecodeObject::InitializeDecoder(u32 sample_rate, u32 channel_count) {
+    if (!state_valid) {
+        return OPUS_INVALID_STATE;
+    }
+
+    if (initialized) {
+        return OPUS_OK;
+    }
+
+    // Unfortunately libopus does not expose the OpusDecoder struct publicly, so we can't include
+    // it in this class. Nintendo does not allocate memory, which is why we have a workbuffer
+    // provided.
+    // We could use _create and have libopus allocate it for us, but then we have to separately
+    // track which decoder is being used between this and multistream in order to call the correct
+    // destroy from the host side.
+    // This is a bit cringe, but is safe as these objects are only ever initialized inside the given
+    // workbuffer, and GetWorkBufferSize will guarantee there's enough space to follow.
+    decoder = (LibOpusDecoder*)(this + 1);
+    s32 ret = opus_decoder_init(decoder, sample_rate, channel_count);
+    if (ret == OPUS_OK) {
+        magic = DecodeObjectMagic;
+        initialized = true;
+        state_valid = true;
+        self = this;
+        final_range = 0;
+    }
+    return ret;
+}
+
+s32 OpusDecodeObject::Shutdown() {
+    if (!state_valid) {
+        return OPUS_INVALID_STATE;
+    }
+
+    if (initialized) {
+        magic = 0x0;
+        initialized = false;
+        state_valid = false;
+        self = nullptr;
+        final_range = 0;
+        decoder = nullptr;
+    }
+    return OPUS_OK;
+}
+
+s32 OpusDecodeObject::ResetDecoder() {
+    return opus_decoder_ctl(decoder, OPUS_RESET_STATE);
+}
+
+s32 OpusDecodeObject::Decode(u32& out_sample_count, u64 output_data, u64 output_data_size,
+                             u64 input_data, u64 input_data_size) {
+    ASSERT(initialized);
+    out_sample_count = 0;
+
+    if (!state_valid) {
+        return OPUS_INVALID_STATE;
+    }
+
+    auto ret_code_or_samples = opus_decode(
+        decoder, reinterpret_cast<const u8*>(input_data), static_cast<opus_int32>(input_data_size),
+        reinterpret_cast<opus_int16*>(output_data), static_cast<opus_int32>(output_data_size), 0);
+
+    if (ret_code_or_samples < OPUS_OK) {
+        return ret_code_or_samples;
+    }
+
+    out_sample_count = ret_code_or_samples;
+    return opus_decoder_ctl(decoder, OPUS_GET_FINAL_RANGE_REQUEST, &final_range);
+}
+
+} // namespace AudioCore::ADSP::OpusDecoder
diff --git a/src/audio_core/adsp/apps/opus/opus_decode_object.h b/src/audio_core/adsp/apps/opus/opus_decode_object.h
new file mode 100644
index 0000000000..6425f987c6
--- /dev/null
+++ b/src/audio_core/adsp/apps/opus/opus_decode_object.h
@@ -0,0 +1,38 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <opus.h>
+
+#include "common/common_types.h"
+
+namespace AudioCore::ADSP::OpusDecoder {
+using LibOpusDecoder = ::OpusDecoder;
+static constexpr u32 DecodeObjectMagic = 0xDEADBEEF;
+
+class OpusDecodeObject {
+public:
+    static u32 GetWorkBufferSize(u32 channel_count);
+    static OpusDecodeObject& Initialize(u64 buffer, u64 buffer2);
+
+    s32 InitializeDecoder(u32 sample_rate, u32 channel_count);
+    s32 Shutdown();
+    s32 ResetDecoder();
+    s32 Decode(u32& out_sample_count, u64 output_data, u64 output_data_size, u64 input_data,
+               u64 input_data_size);
+    u32 GetFinalRange() const noexcept {
+        return final_range;
+    }
+
+private:
+    u32 magic;
+    bool initialized;
+    bool state_valid;
+    OpusDecodeObject* self;
+    u32 final_range;
+    LibOpusDecoder* decoder;
+};
+static_assert(std::is_trivially_constructible_v<OpusDecodeObject>);
+
+} // namespace AudioCore::ADSP::OpusDecoder
diff --git a/src/audio_core/adsp/apps/opus/opus_decoder.cpp b/src/audio_core/adsp/apps/opus/opus_decoder.cpp
new file mode 100644
index 0000000000..2084de1285
--- /dev/null
+++ b/src/audio_core/adsp/apps/opus/opus_decoder.cpp
@@ -0,0 +1,269 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <array>
+#include <chrono>
+
+#include "audio_core/adsp/apps/opus/opus_decode_object.h"
+#include "audio_core/adsp/apps/opus/opus_multistream_decode_object.h"
+#include "audio_core/adsp/apps/opus/shared_memory.h"
+#include "audio_core/audio_core.h"
+#include "audio_core/common/common.h"
+#include "common/logging/log.h"
+#include "common/microprofile.h"
+#include "common/thread.h"
+#include "core/core.h"
+#include "core/core_timing.h"
+
+MICROPROFILE_DEFINE(OpusDecoder, "Audio", "DSP_OpusDecoder", MP_RGB(60, 19, 97));
+
+namespace AudioCore::ADSP::OpusDecoder {
+
+namespace {
+constexpr size_t OpusStreamCountMax = 255;
+
+bool IsValidChannelCount(u32 channel_count) {
+    return channel_count == 1 || channel_count == 2;
+}
+
+bool IsValidMultiStreamChannelCount(u32 channel_count) {
+    return channel_count <= OpusStreamCountMax;
+}
+
+bool IsValidMultiStreamStreamCounts(s32 total_stream_count, s32 sterero_stream_count) {
+    return IsValidMultiStreamChannelCount(total_stream_count) && total_stream_count > 0 &&
+           sterero_stream_count > 0 && sterero_stream_count <= total_stream_count;
+}
+} // namespace
+
+OpusDecoder::OpusDecoder(Core::System& system_) : system{system_} {
+    init_thread = std::jthread([this](std::stop_token stop_token) { Init(stop_token); });
+}
+
+OpusDecoder::~OpusDecoder() {
+    if (!running) {
+        init_thread.request_stop();
+        return;
+    }
+
+    // Shutdown the thread
+    Send(Direction::DSP, Message::Shutdown);
+    auto msg = Receive(Direction::Host);
+    ASSERT_MSG(msg == Message::ShutdownOK, "Expected Opus shutdown code {}, got {}",
+               Message::ShutdownOK, msg);
+    main_thread.request_stop();
+    main_thread.join();
+    running = false;
+}
+
+void OpusDecoder::Send(Direction dir, u32 message) {
+    mailbox.Send(dir, std::move(message));
+}
+
+u32 OpusDecoder::Receive(Direction dir, std::stop_token stop_token) {
+    return mailbox.Receive(dir, stop_token);
+}
+
+void OpusDecoder::Init(std::stop_token stop_token) {
+    Common::SetCurrentThreadName("DSP_OpusDecoder_Init");
+
+    if (Receive(Direction::DSP, stop_token) != Message::Start) {
+        LOG_ERROR(Service_Audio,
+                  "DSP OpusDecoder failed to receive Start message. Opus initialization failed.");
+        return;
+    }
+    main_thread = std::jthread([this](std::stop_token st) { Main(st); });
+    running = true;
+    Send(Direction::Host, Message::StartOK);
+}
+
+void OpusDecoder::Main(std::stop_token stop_token) {
+    Common::SetCurrentThreadName("DSP_OpusDecoder_Main");
+
+    while (!stop_token.stop_requested()) {
+        auto msg = Receive(Direction::DSP, stop_token);
+        switch (msg) {
+        case Shutdown:
+            Send(Direction::Host, Message::ShutdownOK);
+            return;
+
+        case GetWorkBufferSize: {
+            auto channel_count = static_cast<s32>(shared_memory->host_send_data[0]);
+
+            ASSERT(IsValidChannelCount(channel_count));
+
+            shared_memory->dsp_return_data[0] = OpusDecodeObject::GetWorkBufferSize(channel_count);
+            Send(Direction::Host, Message::GetWorkBufferSizeOK);
+        } break;
+
+        case InitializeDecodeObject: {
+            auto buffer = shared_memory->host_send_data[0];
+            auto buffer_size = shared_memory->host_send_data[1];
+            auto sample_rate = static_cast<s32>(shared_memory->host_send_data[2]);
+            auto channel_count = static_cast<s32>(shared_memory->host_send_data[3]);
+
+            ASSERT(sample_rate >= 0);
+            ASSERT(IsValidChannelCount(channel_count));
+            ASSERT(buffer_size >= OpusDecodeObject::GetWorkBufferSize(channel_count));
+
+            auto& decoder_object = OpusDecodeObject::Initialize(buffer, buffer);
+            shared_memory->dsp_return_data[0] =
+                decoder_object.InitializeDecoder(sample_rate, channel_count);
+
+            Send(Direction::Host, Message::InitializeDecodeObjectOK);
+        } break;
+
+        case ShutdownDecodeObject: {
+            auto buffer = shared_memory->host_send_data[0];
+            [[maybe_unused]] auto buffer_size = shared_memory->host_send_data[1];
+
+            auto& decoder_object = OpusDecodeObject::Initialize(buffer, buffer);
+            shared_memory->dsp_return_data[0] = decoder_object.Shutdown();
+
+            Send(Direction::Host, Message::ShutdownDecodeObjectOK);
+        } break;
+
+        case DecodeInterleaved: {
+            auto start_time = system.CoreTiming().GetGlobalTimeUs();
+
+            auto buffer = shared_memory->host_send_data[0];
+            auto input_data = shared_memory->host_send_data[1];
+            auto input_data_size = shared_memory->host_send_data[2];
+            auto output_data = shared_memory->host_send_data[3];
+            auto output_data_size = shared_memory->host_send_data[4];
+            auto final_range = static_cast<u32>(shared_memory->host_send_data[5]);
+            auto reset_requested = shared_memory->host_send_data[6];
+
+            u32 decoded_samples{0};
+
+            auto& decoder_object = OpusDecodeObject::Initialize(buffer, buffer);
+            s32 error_code{OPUS_OK};
+            if (reset_requested) {
+                error_code = decoder_object.ResetDecoder();
+            }
+
+            if (error_code == OPUS_OK) {
+                error_code = decoder_object.Decode(decoded_samples, output_data, output_data_size,
+                                                   input_data, input_data_size);
+            }
+
+            if (error_code == OPUS_OK) {
+                if (final_range && decoder_object.GetFinalRange() != final_range) {
+                    error_code = OPUS_INVALID_PACKET;
+                }
+            }
+
+            auto end_time = system.CoreTiming().GetGlobalTimeUs();
+            shared_memory->dsp_return_data[0] = error_code;
+            shared_memory->dsp_return_data[1] = decoded_samples;
+            shared_memory->dsp_return_data[2] = (end_time - start_time).count();
+
+            Send(Direction::Host, Message::DecodeInterleavedOK);
+        } break;
+
+        case MapMemory: {
+            [[maybe_unused]] auto buffer = shared_memory->host_send_data[0];
+            [[maybe_unused]] auto buffer_size = shared_memory->host_send_data[1];
+            Send(Direction::Host, Message::MapMemoryOK);
+        } break;
+
+        case UnmapMemory: {
+            [[maybe_unused]] auto buffer = shared_memory->host_send_data[0];
+            [[maybe_unused]] auto buffer_size = shared_memory->host_send_data[1];
+            Send(Direction::Host, Message::UnmapMemoryOK);
+        } break;
+
+        case GetWorkBufferSizeForMultiStream: {
+            auto total_stream_count = static_cast<s32>(shared_memory->host_send_data[0]);
+            auto stereo_stream_count = static_cast<s32>(shared_memory->host_send_data[1]);
+
+            ASSERT(IsValidMultiStreamStreamCounts(total_stream_count, stereo_stream_count));
+
+            shared_memory->dsp_return_data[0] = OpusMultiStreamDecodeObject::GetWorkBufferSize(
+                total_stream_count, stereo_stream_count);
+            Send(Direction::Host, Message::GetWorkBufferSizeForMultiStreamOK);
+        } break;
+
+        case InitializeMultiStreamDecodeObject: {
+            auto buffer = shared_memory->host_send_data[0];
+            auto buffer_size = shared_memory->host_send_data[1];
+            auto sample_rate = static_cast<s32>(shared_memory->host_send_data[2]);
+            auto channel_count = static_cast<s32>(shared_memory->host_send_data[3]);
+            auto total_stream_count = static_cast<s32>(shared_memory->host_send_data[4]);
+            auto stereo_stream_count = static_cast<s32>(shared_memory->host_send_data[5]);
+            // Nintendo seem to have a bug here, they try to use &host_send_data[6] for the channel
+            // mappings, but [6] is never set, and there is not enough room in the argument data for
+            // more than 40 channels, when 255 are possible.
+            // It also means the mapping values are undefined, though likely always 0,
+            // and the mappings given by the game are ignored. The mappings are copied to this
+            // dedicated buffer host side, so let's do as intended.
+            auto mappings = shared_memory->channel_mapping.data();
+
+            ASSERT(IsValidMultiStreamStreamCounts(total_stream_count, stereo_stream_count));
+            ASSERT(sample_rate >= 0);
+            ASSERT(buffer_size >= OpusMultiStreamDecodeObject::GetWorkBufferSize(
+                                      total_stream_count, stereo_stream_count));
+
+            auto& decoder_object = OpusMultiStreamDecodeObject::Initialize(buffer, buffer);
+            shared_memory->dsp_return_data[0] = decoder_object.InitializeDecoder(
+                sample_rate, total_stream_count, channel_count, stereo_stream_count, mappings);
+
+            Send(Direction::Host, Message::InitializeMultiStreamDecodeObjectOK);
+        } break;
+
+        case ShutdownMultiStreamDecodeObject: {
+            auto buffer = shared_memory->host_send_data[0];
+            [[maybe_unused]] auto buffer_size = shared_memory->host_send_data[1];
+
+            auto& decoder_object = OpusMultiStreamDecodeObject::Initialize(buffer, buffer);
+            shared_memory->dsp_return_data[0] = decoder_object.Shutdown();
+
+            Send(Direction::Host, Message::ShutdownMultiStreamDecodeObjectOK);
+        } break;
+
+        case DecodeInterleavedForMultiStream: {
+            auto start_time = system.CoreTiming().GetGlobalTimeUs();
+
+            auto buffer = shared_memory->host_send_data[0];
+            auto input_data = shared_memory->host_send_data[1];
+            auto input_data_size = shared_memory->host_send_data[2];
+            auto output_data = shared_memory->host_send_data[3];
+            auto output_data_size = shared_memory->host_send_data[4];
+            auto final_range = static_cast<u32>(shared_memory->host_send_data[5]);
+            auto reset_requested = shared_memory->host_send_data[6];
+
+            u32 decoded_samples{0};
+
+            auto& decoder_object = OpusMultiStreamDecodeObject::Initialize(buffer, buffer);
+            s32 error_code{OPUS_OK};
+            if (reset_requested) {
+                error_code = decoder_object.ResetDecoder();
+            }
+
+            if (error_code == OPUS_OK) {
+                error_code = decoder_object.Decode(decoded_samples, output_data, output_data_size,
+                                                   input_data, input_data_size);
+            }
+
+            if (error_code == OPUS_OK) {
+                if (final_range && decoder_object.GetFinalRange() != final_range) {
+                    error_code = OPUS_INVALID_PACKET;
+                }
+            }
+
+            auto end_time = system.CoreTiming().GetGlobalTimeUs();
+            shared_memory->dsp_return_data[0] = error_code;
+            shared_memory->dsp_return_data[1] = decoded_samples;
+            shared_memory->dsp_return_data[2] = (end_time - start_time).count();
+
+            Send(Direction::Host, Message::DecodeInterleavedForMultiStreamOK);
+        } break;
+
+        default:
+            LOG_ERROR(Service_Audio, "Invalid OpusDecoder command {}", msg);
+            continue;
+        }
+    }
+}
+
+} // namespace AudioCore::ADSP::OpusDecoder
diff --git a/src/audio_core/adsp/apps/opus/opus_decoder.h b/src/audio_core/adsp/apps/opus/opus_decoder.h
new file mode 100644
index 0000000000..fcb89bb40e
--- /dev/null
+++ b/src/audio_core/adsp/apps/opus/opus_decoder.h
@@ -0,0 +1,92 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <memory>
+#include <thread>
+
+#include "audio_core/adsp/apps/opus/shared_memory.h"
+#include "audio_core/adsp/mailbox.h"
+#include "common/common_types.h"
+
+namespace Core {
+class System;
+} // namespace Core
+
+namespace AudioCore::ADSP::OpusDecoder {
+
+enum Message : u32 {
+    Invalid = 0,
+    Start = 1,
+    Shutdown = 2,
+    StartOK = 11,
+    ShutdownOK = 12,
+    GetWorkBufferSize = 21,
+    InitializeDecodeObject = 22,
+    ShutdownDecodeObject = 23,
+    DecodeInterleaved = 24,
+    MapMemory = 25,
+    UnmapMemory = 26,
+    GetWorkBufferSizeForMultiStream = 27,
+    InitializeMultiStreamDecodeObject = 28,
+    ShutdownMultiStreamDecodeObject = 29,
+    DecodeInterleavedForMultiStream = 30,
+
+    GetWorkBufferSizeOK = 41,
+    InitializeDecodeObjectOK = 42,
+    ShutdownDecodeObjectOK = 43,
+    DecodeInterleavedOK = 44,
+    MapMemoryOK = 45,
+    UnmapMemoryOK = 46,
+    GetWorkBufferSizeForMultiStreamOK = 47,
+    InitializeMultiStreamDecodeObjectOK = 48,
+    ShutdownMultiStreamDecodeObjectOK = 49,
+    DecodeInterleavedForMultiStreamOK = 50,
+};
+
+/**
+ * The AudioRenderer application running on the ADSP.
+ */
+class OpusDecoder {
+public:
+    explicit OpusDecoder(Core::System& system);
+    ~OpusDecoder();
+
+    bool IsRunning() const noexcept {
+        return running;
+    }
+
+    void Send(Direction dir, u32 message);
+    u32 Receive(Direction dir, std::stop_token stop_token = {});
+
+    void SetSharedMemory(SharedMemory& shared_memory_) {
+        shared_memory = &shared_memory_;
+    }
+
+private:
+    /**
+     * Initializing thread, launched at audio_core boot to avoid blocking the main emu boot thread.
+     */
+    void Init(std::stop_token stop_token);
+    /**
+     * Main OpusDecoder thread, responsible for processing the incoming Opus packets.
+     */
+    void Main(std::stop_token stop_token);
+
+    /// Core system
+    Core::System& system;
+    /// Mailbox to communicate messages with the host, drives the main thread
+    Mailbox mailbox;
+    /// Init thread
+    std::jthread init_thread{};
+    /// Main thread
+    std::jthread main_thread{};
+    /// The current state
+    bool running{};
+    /// Structure shared with the host, input data set by the host before sending a mailbox message,
+    /// and the responses are written back by the OpusDecoder.
+    SharedMemory* shared_memory{};
+};
+
+} // namespace AudioCore::ADSP::OpusDecoder
diff --git a/src/audio_core/adsp/apps/opus/opus_multistream_decode_object.cpp b/src/audio_core/adsp/apps/opus/opus_multistream_decode_object.cpp
new file mode 100644
index 0000000000..f6d362e68c
--- /dev/null
+++ b/src/audio_core/adsp/apps/opus/opus_multistream_decode_object.cpp
@@ -0,0 +1,111 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/adsp/apps/opus/opus_multistream_decode_object.h"
+#include "common/assert.h"
+
+namespace AudioCore::ADSP::OpusDecoder {
+
+namespace {
+bool IsValidChannelCount(u32 channel_count) {
+    return channel_count == 1 || channel_count == 2;
+}
+
+bool IsValidStreamCounts(u32 total_stream_count, u32 stereo_stream_count) {
+    return total_stream_count > 0 && stereo_stream_count > 0 &&
+           stereo_stream_count <= total_stream_count && IsValidChannelCount(total_stream_count);
+}
+} // namespace
+
+u32 OpusMultiStreamDecodeObject::GetWorkBufferSize(u32 total_stream_count,
+                                                   u32 stereo_stream_count) {
+    if (IsValidStreamCounts(total_stream_count, stereo_stream_count)) {
+        return static_cast<u32>(sizeof(OpusMultiStreamDecodeObject)) +
+               opus_multistream_decoder_get_size(total_stream_count, stereo_stream_count);
+    }
+    return 0;
+}
+
+OpusMultiStreamDecodeObject& OpusMultiStreamDecodeObject::Initialize(u64 buffer, u64 buffer2) {
+    auto* new_decoder = reinterpret_cast<OpusMultiStreamDecodeObject*>(buffer);
+    auto* comparison = reinterpret_cast<OpusMultiStreamDecodeObject*>(buffer2);
+
+    if (new_decoder->magic == DecodeMultiStreamObjectMagic) {
+        if (!new_decoder->initialized ||
+            (new_decoder->initialized && new_decoder->self == comparison)) {
+            new_decoder->state_valid = true;
+        }
+    } else {
+        new_decoder->initialized = false;
+        new_decoder->state_valid = true;
+    }
+    return *new_decoder;
+}
+
+s32 OpusMultiStreamDecodeObject::InitializeDecoder(u32 sample_rate, u32 total_stream_count,
+                                                   u32 channel_count, u32 stereo_stream_count,
+                                                   u8* mappings) {
+    if (!state_valid) {
+        return OPUS_INVALID_STATE;
+    }
+
+    if (initialized) {
+        return OPUS_OK;
+    }
+
+    // See OpusDecodeObject::InitializeDecoder for an explanation of this
+    decoder = (LibOpusMSDecoder*)(this + 1);
+    s32 ret = opus_multistream_decoder_init(decoder, sample_rate, channel_count, total_stream_count,
+                                            stereo_stream_count, mappings);
+    if (ret == OPUS_OK) {
+        magic = DecodeMultiStreamObjectMagic;
+        initialized = true;
+        state_valid = true;
+        self = this;
+        final_range = 0;
+    }
+    return ret;
+}
+
+s32 OpusMultiStreamDecodeObject::Shutdown() {
+    if (!state_valid) {
+        return OPUS_INVALID_STATE;
+    }
+
+    if (initialized) {
+        magic = 0x0;
+        initialized = false;
+        state_valid = false;
+        self = nullptr;
+        final_range = 0;
+        decoder = nullptr;
+    }
+    return OPUS_OK;
+}
+
+s32 OpusMultiStreamDecodeObject::ResetDecoder() {
+    return opus_multistream_decoder_ctl(decoder, OPUS_RESET_STATE);
+}
+
+s32 OpusMultiStreamDecodeObject::Decode(u32& out_sample_count, u64 output_data,
+                                        u64 output_data_size, u64 input_data, u64 input_data_size) {
+    ASSERT(initialized);
+    out_sample_count = 0;
+
+    if (!state_valid) {
+        return OPUS_INVALID_STATE;
+    }
+
+    auto ret_code_or_samples = opus_multistream_decode(
+        decoder, reinterpret_cast<const u8*>(input_data), static_cast<opus_int32>(input_data_size),
+        reinterpret_cast<opus_int16*>(output_data), static_cast<opus_int32>(output_data_size), 0);
+
+    if (ret_code_or_samples < OPUS_OK) {
+        return ret_code_or_samples;
+    }
+
+    out_sample_count = ret_code_or_samples;
+    return opus_multistream_decoder_ctl(decoder, OPUS_GET_FINAL_RANGE_REQUEST, &final_range);
+}
+
+} // namespace AudioCore::ADSP::OpusDecoder
diff --git a/src/audio_core/adsp/apps/opus/opus_multistream_decode_object.h b/src/audio_core/adsp/apps/opus/opus_multistream_decode_object.h
new file mode 100644
index 0000000000..93558ded5d
--- /dev/null
+++ b/src/audio_core/adsp/apps/opus/opus_multistream_decode_object.h
@@ -0,0 +1,39 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <opus_multistream.h>
+
+#include "common/common_types.h"
+
+namespace AudioCore::ADSP::OpusDecoder {
+using LibOpusMSDecoder = ::OpusMSDecoder;
+static constexpr u32 DecodeMultiStreamObjectMagic = 0xDEADBEEF;
+
+class OpusMultiStreamDecodeObject {
+public:
+    static u32 GetWorkBufferSize(u32 total_stream_count, u32 stereo_stream_count);
+    static OpusMultiStreamDecodeObject& Initialize(u64 buffer, u64 buffer2);
+
+    s32 InitializeDecoder(u32 sample_rate, u32 total_stream_count, u32 channel_count,
+                          u32 stereo_stream_count, u8* mappings);
+    s32 Shutdown();
+    s32 ResetDecoder();
+    s32 Decode(u32& out_sample_count, u64 output_data, u64 output_data_size, u64 input_data,
+               u64 input_data_size);
+    u32 GetFinalRange() const noexcept {
+        return final_range;
+    }
+
+private:
+    u32 magic;
+    bool initialized;
+    bool state_valid;
+    OpusMultiStreamDecodeObject* self;
+    u32 final_range;
+    LibOpusMSDecoder* decoder;
+};
+static_assert(std::is_trivially_constructible_v<OpusMultiStreamDecodeObject>);
+
+} // namespace AudioCore::ADSP::OpusDecoder
diff --git a/src/audio_core/adsp/apps/opus/shared_memory.h b/src/audio_core/adsp/apps/opus/shared_memory.h
new file mode 100644
index 0000000000..c696731ed8
--- /dev/null
+++ b/src/audio_core/adsp/apps/opus/shared_memory.h
@@ -0,0 +1,17 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "common/common_funcs.h"
+#include "common/common_types.h"
+
+namespace AudioCore::ADSP::OpusDecoder {
+
+struct SharedMemory {
+    std::array<u8, 0x100> channel_mapping{};
+    std::array<u64, 16> host_send_data{};
+    std::array<u64, 16> dsp_return_data{};
+};
+
+} // namespace AudioCore::ADSP::OpusDecoder
diff --git a/src/audio_core/adsp/mailbox.h b/src/audio_core/adsp/mailbox.h
index c31b737177..1dd40ebfa7 100644
--- a/src/audio_core/adsp/mailbox.h
+++ b/src/audio_core/adsp/mailbox.h
@@ -3,6 +3,8 @@
 
 #pragma once
 
+#include <span>
+
 #include "common/bounded_threadsafe_queue.h"
 #include "common/common_types.h"
 
@@ -19,11 +21,6 @@ enum class Direction : u32 {
     DSP,
 };
 
-struct MailboxMessage {
-    u32 msg;
-    std::span<u8> data;
-};
-
 class Mailbox {
 public:
     void Initialize(AppMailboxId id_) {
@@ -35,25 +32,19 @@ public:
         return id;
     }
 
-    void Send(Direction dir, MailboxMessage&& message) {
+    void Send(Direction dir, u32 message) {
         auto& queue = dir == Direction::Host ? host_queue : adsp_queue;
-        queue.EmplaceWait(std::move(message));
+        queue.EmplaceWait(message);
     }
 
-    MailboxMessage Receive(Direction dir, bool block = true) {
+    u32 Receive(Direction dir, std::stop_token stop_token = {}) {
         auto& queue = dir == Direction::Host ? host_queue : adsp_queue;
-        MailboxMessage t;
-        if (block) {
-            queue.PopWait(t);
-        } else {
-            queue.TryPop(t);
-        }
-        return t;
+        return queue.PopWait(stop_token);
     }
 
     void Reset() {
         id = AppMailboxId::Invalid;
-        MailboxMessage t;
+        u32 t{};
         while (host_queue.TryPop(t)) {
         }
         while (adsp_queue.TryPop(t)) {
@@ -62,8 +53,8 @@ public:
 
 private:
     AppMailboxId id{0};
-    Common::SPSCQueue<MailboxMessage> host_queue;
-    Common::SPSCQueue<MailboxMessage> adsp_queue;
+    Common::SPSCQueue<u32> host_queue;
+    Common::SPSCQueue<u32> adsp_queue;
 };
 
 } // namespace AudioCore::ADSP
diff --git a/src/audio_core/opus/decoder.cpp b/src/audio_core/opus/decoder.cpp
new file mode 100644
index 0000000000..5b23fce148
--- /dev/null
+++ b/src/audio_core/opus/decoder.cpp
@@ -0,0 +1,179 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/opus/decoder.h"
+#include "audio_core/opus/hardware_opus.h"
+#include "audio_core/opus/parameters.h"
+#include "common/alignment.h"
+#include "common/swap.h"
+#include "core/core.h"
+
+namespace AudioCore::OpusDecoder {
+using namespace Service::Audio;
+namespace {
+OpusPacketHeader ReverseHeader(OpusPacketHeader header) {
+    OpusPacketHeader out;
+    out.size = Common::swap32(header.size);
+    out.final_range = Common::swap32(header.final_range);
+    return out;
+}
+} // namespace
+
+OpusDecoder::OpusDecoder(Core::System& system_, HardwareOpus& hardware_opus_)
+    : system{system_}, hardware_opus{hardware_opus_} {}
+
+OpusDecoder::~OpusDecoder() {
+    if (decode_object_initialized) {
+        hardware_opus.ShutdownDecodeObject(shared_buffer.get(), shared_buffer_size);
+    }
+}
+
+Result OpusDecoder::Initialize(OpusParametersEx& params, Kernel::KTransferMemory* transfer_memory,
+                               u64 transfer_memory_size) {
+    auto frame_size{params.use_large_frame_size ? 5760 : 1920};
+    shared_buffer_size = transfer_memory_size;
+    shared_buffer = std::make_unique<u8[]>(shared_buffer_size);
+    shared_memory_mapped = true;
+
+    buffer_size =
+        Common::AlignUp((frame_size * params.channel_count) / (48'000 / params.sample_rate), 16);
+
+    out_data = {shared_buffer.get() + shared_buffer_size - buffer_size, buffer_size};
+    size_t in_data_size{0x600u};
+    in_data = {out_data.data() - in_data_size, in_data_size};
+
+    ON_RESULT_FAILURE {
+        if (shared_memory_mapped) {
+            shared_memory_mapped = false;
+            ASSERT(R_SUCCEEDED(hardware_opus.UnmapMemory(shared_buffer.get(), shared_buffer_size)));
+        }
+    };
+
+    R_TRY(hardware_opus.InitializeDecodeObject(params.sample_rate, params.channel_count,
+                                               shared_buffer.get(), shared_buffer_size));
+
+    sample_rate = params.sample_rate;
+    channel_count = params.channel_count;
+    use_large_frame_size = params.use_large_frame_size;
+    decode_object_initialized = true;
+    R_SUCCEED();
+}
+
+Result OpusDecoder::Initialize(OpusMultiStreamParametersEx& params,
+                               Kernel::KTransferMemory* transfer_memory, u64 transfer_memory_size) {
+    auto frame_size{params.use_large_frame_size ? 5760 : 1920};
+    shared_buffer_size = transfer_memory_size;
+    shared_buffer = std::make_unique<u8[]>(shared_buffer_size);
+    shared_memory_mapped = true;
+
+    buffer_size =
+        Common::AlignUp((frame_size * params.channel_count) / (48'000 / params.sample_rate), 16);
+
+    out_data = {shared_buffer.get() + shared_buffer_size - buffer_size, buffer_size};
+    size_t in_data_size{Common::AlignUp(1500ull * params.total_stream_count, 64u)};
+    in_data = {out_data.data() - in_data_size, in_data_size};
+
+    ON_RESULT_FAILURE {
+        if (shared_memory_mapped) {
+            shared_memory_mapped = false;
+            ASSERT(R_SUCCEEDED(hardware_opus.UnmapMemory(shared_buffer.get(), shared_buffer_size)));
+        }
+    };
+
+    R_TRY(hardware_opus.InitializeMultiStreamDecodeObject(
+        params.sample_rate, params.channel_count, params.total_stream_count,
+        params.stereo_stream_count, params.mappings.data(), shared_buffer.get(),
+        shared_buffer_size));
+
+    sample_rate = params.sample_rate;
+    channel_count = params.channel_count;
+    total_stream_count = params.total_stream_count;
+    stereo_stream_count = params.stereo_stream_count;
+    use_large_frame_size = params.use_large_frame_size;
+    decode_object_initialized = true;
+    R_SUCCEED();
+}
+
+Result OpusDecoder::DecodeInterleaved(u32* out_data_size, u64* out_time_taken,
+                                      u32* out_sample_count, std::span<const u8> input_data,
+                                      std::span<u8> output_data, bool reset) {
+    u32 out_samples;
+    u64 time_taken{};
+
+    R_UNLESS(input_data.size_bytes() > sizeof(OpusPacketHeader), ResultInputDataTooSmall);
+
+    auto* header_p{reinterpret_cast<const OpusPacketHeader*>(input_data.data())};
+    OpusPacketHeader header{ReverseHeader(*header_p)};
+
+    R_UNLESS(in_data.size_bytes() >= header.size &&
+                 header.size + sizeof(OpusPacketHeader) <= input_data.size_bytes(),
+             ResultBufferTooSmall);
+
+    if (!shared_memory_mapped) {
+        R_TRY(hardware_opus.MapMemory(shared_buffer.get(), shared_buffer_size));
+        shared_memory_mapped = true;
+    }
+
+    std::memcpy(in_data.data(), input_data.data() + sizeof(OpusPacketHeader), header.size);
+
+    R_TRY(hardware_opus.DecodeInterleaved(out_samples, out_data.data(), out_data.size_bytes(),
+                                          channel_count, in_data.data(), header.size,
+                                          shared_buffer.get(), time_taken, reset));
+
+    std::memcpy(output_data.data(), out_data.data(), out_samples * channel_count * sizeof(s16));
+
+    *out_data_size = header.size + sizeof(OpusPacketHeader);
+    *out_sample_count = out_samples;
+    if (out_time_taken) {
+        *out_time_taken = time_taken / 1000;
+    }
+    R_SUCCEED();
+}
+
+Result OpusDecoder::SetContext([[maybe_unused]] std::span<const u8> context) {
+    R_SUCCEED_IF(shared_memory_mapped);
+    shared_memory_mapped = true;
+    R_RETURN(hardware_opus.MapMemory(shared_buffer.get(), shared_buffer_size));
+}
+
+Result OpusDecoder::DecodeInterleavedForMultiStream(u32* out_data_size, u64* out_time_taken,
+                                                    u32* out_sample_count,
+                                                    std::span<const u8> input_data,
+                                                    std::span<u8> output_data, bool reset) {
+    u32 out_samples;
+    u64 time_taken{};
+
+    R_UNLESS(input_data.size_bytes() > sizeof(OpusPacketHeader), ResultInputDataTooSmall);
+
+    auto* header_p{reinterpret_cast<const OpusPacketHeader*>(input_data.data())};
+    OpusPacketHeader header{ReverseHeader(*header_p)};
+
+    LOG_ERROR(Service_Audio, "header size 0x{:X} input data size 0x{:X} in_data size 0x{:X}",
+              header.size, input_data.size_bytes(), in_data.size_bytes());
+
+    R_UNLESS(in_data.size_bytes() >= header.size &&
+                 header.size + sizeof(OpusPacketHeader) <= input_data.size_bytes(),
+             ResultBufferTooSmall);
+
+    if (!shared_memory_mapped) {
+        R_TRY(hardware_opus.MapMemory(shared_buffer.get(), shared_buffer_size));
+        shared_memory_mapped = true;
+    }
+
+    std::memcpy(in_data.data(), input_data.data() + sizeof(OpusPacketHeader), header.size);
+
+    R_TRY(hardware_opus.DecodeInterleavedForMultiStream(
+        out_samples, out_data.data(), out_data.size_bytes(), channel_count, in_data.data(),
+        header.size, shared_buffer.get(), time_taken, reset));
+
+    std::memcpy(output_data.data(), out_data.data(), out_samples * channel_count * sizeof(s16));
+
+    *out_data_size = header.size + sizeof(OpusPacketHeader);
+    *out_sample_count = out_samples;
+    if (out_time_taken) {
+        *out_time_taken = time_taken / 1000;
+    }
+    R_SUCCEED();
+}
+
+} // namespace AudioCore::OpusDecoder
diff --git a/src/audio_core/opus/decoder.h b/src/audio_core/opus/decoder.h
new file mode 100644
index 0000000000..d08d8a4a4d
--- /dev/null
+++ b/src/audio_core/opus/decoder.h
@@ -0,0 +1,53 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <span>
+
+#include "audio_core/opus/parameters.h"
+#include "common/common_types.h"
+#include "core/hle/kernel/k_transfer_memory.h"
+#include "core/hle/service/audio/errors.h"
+
+namespace Core {
+class System;
+}
+
+namespace AudioCore::OpusDecoder {
+class HardwareOpus;
+
+class OpusDecoder {
+public:
+    explicit OpusDecoder(Core::System& system, HardwareOpus& hardware_opus_);
+    ~OpusDecoder();
+
+    Result Initialize(OpusParametersEx& params, Kernel::KTransferMemory* transfer_memory,
+                      u64 transfer_memory_size);
+    Result Initialize(OpusMultiStreamParametersEx& params, Kernel::KTransferMemory* transfer_memory,
+                      u64 transfer_memory_size);
+    Result DecodeInterleaved(u32* out_data_size, u64* out_time_taken, u32* out_sample_count,
+                             std::span<const u8> input_data, std::span<u8> output_data, bool reset);
+    Result SetContext([[maybe_unused]] std::span<const u8> context);
+    Result DecodeInterleavedForMultiStream(u32* out_data_size, u64* out_time_taken,
+                                           u32* out_sample_count, std::span<const u8> input_data,
+                                           std::span<u8> output_data, bool reset);
+
+private:
+    Core::System& system;
+    HardwareOpus& hardware_opus;
+    std::unique_ptr<u8[]> shared_buffer{};
+    u64 shared_buffer_size;
+    std::span<u8> in_data{};
+    std::span<u8> out_data{};
+    u64 buffer_size{};
+    s32 sample_rate{};
+    s32 channel_count{};
+    bool use_large_frame_size{false};
+    s32 total_stream_count{};
+    s32 stereo_stream_count{};
+    bool shared_memory_mapped{false};
+    bool decode_object_initialized{false};
+};
+
+} // namespace AudioCore::OpusDecoder
diff --git a/src/audio_core/opus/decoder_manager.cpp b/src/audio_core/opus/decoder_manager.cpp
new file mode 100644
index 0000000000..4a5382973a
--- /dev/null
+++ b/src/audio_core/opus/decoder_manager.cpp
@@ -0,0 +1,102 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include "audio_core/adsp/apps/opus/opus_decoder.h"
+#include "audio_core/opus/decoder_manager.h"
+#include "common/alignment.h"
+#include "core/core.h"
+
+namespace AudioCore::OpusDecoder {
+using namespace Service::Audio;
+
+namespace {
+bool IsValidChannelCount(u32 channel_count) {
+    return channel_count == 1 || channel_count == 2;
+}
+
+bool IsValidMultiStreamChannelCount(u32 channel_count) {
+    return channel_count > 0 && channel_count <= OpusStreamCountMax;
+}
+
+bool IsValidSampleRate(u32 sample_rate) {
+    return sample_rate == 8'000 || sample_rate == 12'000 || sample_rate == 16'000 ||
+           sample_rate == 24'000 || sample_rate == 48'000;
+}
+
+bool IsValidStreamCount(u32 channel_count, u32 total_stream_count, u32 stereo_stream_count) {
+    return total_stream_count > 0 && stereo_stream_count > 0 &&
+           stereo_stream_count <= total_stream_count &&
+           total_stream_count + stereo_stream_count <= channel_count;
+}
+
+} // namespace
+
+OpusDecoderManager::OpusDecoderManager(Core::System& system_)
+    : system{system_}, hardware_opus{system} {
+    for (u32 i = 0; i < MaxChannels; i++) {
+        required_workbuffer_sizes[i] = hardware_opus.GetWorkBufferSize(1 + i);
+    }
+}
+
+Result OpusDecoderManager::GetWorkBufferSize(OpusParameters& params, u64& out_size) {
+    OpusParametersEx ex{
+        .sample_rate = params.sample_rate,
+        .channel_count = params.channel_count,
+        .use_large_frame_size = false,
+    };
+    R_RETURN(GetWorkBufferSizeExEx(ex, out_size));
+}
+
+Result OpusDecoderManager::GetWorkBufferSizeEx(OpusParametersEx& params, u64& out_size) {
+    R_RETURN(GetWorkBufferSizeExEx(params, out_size));
+}
+
+Result OpusDecoderManager::GetWorkBufferSizeExEx(OpusParametersEx& params, u64& out_size) {
+    R_UNLESS(IsValidChannelCount(params.channel_count), ResultInvalidOpusChannelCount);
+    R_UNLESS(IsValidSampleRate(params.sample_rate), ResultInvalidOpusSampleRate);
+
+    auto work_buffer_size{required_workbuffer_sizes[params.channel_count - 1]};
+    auto frame_size{params.use_large_frame_size ? 5760 : 1920};
+    work_buffer_size +=
+        Common::AlignUp((frame_size * params.channel_count) / (48'000 / params.sample_rate), 64);
+    out_size = work_buffer_size + 0x600;
+    R_SUCCEED();
+}
+
+Result OpusDecoderManager::GetWorkBufferSizeForMultiStream(OpusMultiStreamParameters& params,
+                                                           u64& out_size) {
+    OpusMultiStreamParametersEx ex{
+        .sample_rate = params.sample_rate,
+        .channel_count = params.channel_count,
+        .total_stream_count = params.total_stream_count,
+        .stereo_stream_count = params.stereo_stream_count,
+        .use_large_frame_size = false,
+        .mappings = {},
+    };
+    R_RETURN(GetWorkBufferSizeForMultiStreamExEx(ex, out_size));
+}
+
+Result OpusDecoderManager::GetWorkBufferSizeForMultiStreamEx(OpusMultiStreamParametersEx& params,
+                                                             u64& out_size) {
+    R_RETURN(GetWorkBufferSizeForMultiStreamExEx(params, out_size));
+}
+
+Result OpusDecoderManager::GetWorkBufferSizeForMultiStreamExEx(OpusMultiStreamParametersEx& params,
+                                                               u64& out_size) {
+    R_UNLESS(IsValidMultiStreamChannelCount(params.channel_count), ResultInvalidOpusChannelCount);
+    R_UNLESS(IsValidSampleRate(params.sample_rate), ResultInvalidOpusSampleRate);
+    R_UNLESS(IsValidStreamCount(params.channel_count, params.total_stream_count,
+                                params.stereo_stream_count),
+             ResultInvalidOpusSampleRate);
+
+    auto work_buffer_size{hardware_opus.GetWorkBufferSizeForMultiStream(
+        params.total_stream_count, params.stereo_stream_count)};
+    auto frame_size{params.use_large_frame_size ? 5760 : 1920};
+    work_buffer_size += Common::AlignUp(1500 * params.total_stream_count, 64);
+    work_buffer_size +=
+        Common::AlignUp((frame_size * params.channel_count) / (48'000 / params.sample_rate), 64);
+    out_size = work_buffer_size;
+    R_SUCCEED();
+}
+
+} // namespace AudioCore::OpusDecoder
diff --git a/src/audio_core/opus/decoder_manager.h b/src/audio_core/opus/decoder_manager.h
new file mode 100644
index 0000000000..466e1967b8
--- /dev/null
+++ b/src/audio_core/opus/decoder_manager.h
@@ -0,0 +1,38 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "audio_core/opus/hardware_opus.h"
+#include "audio_core/opus/parameters.h"
+#include "common/common_types.h"
+#include "core/hle/service/audio/errors.h"
+
+namespace Core {
+class System;
+}
+
+namespace AudioCore::OpusDecoder {
+
+class OpusDecoderManager {
+public:
+    OpusDecoderManager(Core::System& system);
+
+    HardwareOpus& GetHardwareOpus() {
+        return hardware_opus;
+    }
+
+    Result GetWorkBufferSize(OpusParameters& params, u64& out_size);
+    Result GetWorkBufferSizeEx(OpusParametersEx& params, u64& out_size);
+    Result GetWorkBufferSizeExEx(OpusParametersEx& params, u64& out_size);
+    Result GetWorkBufferSizeForMultiStream(OpusMultiStreamParameters& params, u64& out_size);
+    Result GetWorkBufferSizeForMultiStreamEx(OpusMultiStreamParametersEx& params, u64& out_size);
+    Result GetWorkBufferSizeForMultiStreamExEx(OpusMultiStreamParametersEx& params, u64& out_size);
+
+private:
+    Core::System& system;
+    HardwareOpus hardware_opus;
+    std::array<u64, MaxChannels> required_workbuffer_sizes{};
+};
+
+} // namespace AudioCore::OpusDecoder
diff --git a/src/audio_core/opus/hardware_opus.cpp b/src/audio_core/opus/hardware_opus.cpp
new file mode 100644
index 0000000000..d6544dcb05
--- /dev/null
+++ b/src/audio_core/opus/hardware_opus.cpp
@@ -0,0 +1,241 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include <array>
+
+#include "audio_core/audio_core.h"
+#include "audio_core/opus/hardware_opus.h"
+#include "core/core.h"
+
+namespace AudioCore::OpusDecoder {
+namespace {
+using namespace Service::Audio;
+
+static constexpr Result ResultCodeFromLibOpusErrorCode(u64 error_code) {
+    s32 error{static_cast<s32>(error_code)};
+    ASSERT(error <= OPUS_OK);
+    switch (error) {
+    case OPUS_ALLOC_FAIL:
+        R_THROW(ResultLibOpusAllocFail);
+    case OPUS_INVALID_STATE:
+        R_THROW(ResultLibOpusInvalidState);
+    case OPUS_UNIMPLEMENTED:
+        R_THROW(ResultLibOpusUnimplemented);
+    case OPUS_INVALID_PACKET:
+        R_THROW(ResultLibOpusInvalidPacket);
+    case OPUS_INTERNAL_ERROR:
+        R_THROW(ResultLibOpusInternalError);
+    case OPUS_BUFFER_TOO_SMALL:
+        R_THROW(ResultBufferTooSmall);
+    case OPUS_BAD_ARG:
+        R_THROW(ResultLibOpusBadArg);
+    case OPUS_OK:
+        R_RETURN(ResultSuccess);
+    }
+    UNREACHABLE();
+}
+
+} // namespace
+
+HardwareOpus::HardwareOpus(Core::System& system_)
+    : system{system_}, opus_decoder{system.AudioCore().ADSP().OpusDecoder()} {
+    opus_decoder.SetSharedMemory(shared_memory);
+}
+
+u64 HardwareOpus::GetWorkBufferSize(u32 channel) {
+    if (!opus_decoder.IsRunning()) {
+        return 0;
+    }
+    std::scoped_lock l{mutex};
+    shared_memory.host_send_data[0] = channel;
+    opus_decoder.Send(ADSP::Direction::DSP, ADSP::OpusDecoder::Message::GetWorkBufferSize);
+    auto msg = opus_decoder.Receive(ADSP::Direction::Host);
+    if (msg != ADSP::OpusDecoder::Message::GetWorkBufferSizeOK) {
+        LOG_ERROR(Service_Audio, "OpusDecoder returned invalid message. Expected {} got {}",
+                  ADSP::OpusDecoder::Message::GetWorkBufferSizeOK, msg);
+        return 0;
+    }
+    return shared_memory.dsp_return_data[0];
+}
+
+u64 HardwareOpus::GetWorkBufferSizeForMultiStream(u32 total_stream_count, u32 stereo_stream_count) {
+    std::scoped_lock l{mutex};
+    shared_memory.host_send_data[0] = total_stream_count;
+    shared_memory.host_send_data[1] = stereo_stream_count;
+    opus_decoder.Send(ADSP::Direction::DSP,
+                      ADSP::OpusDecoder::Message::GetWorkBufferSizeForMultiStream);
+    auto msg = opus_decoder.Receive(ADSP::Direction::Host);
+    if (msg != ADSP::OpusDecoder::Message::GetWorkBufferSizeForMultiStreamOK) {
+        LOG_ERROR(Service_Audio, "OpusDecoder returned invalid message. Expected {} got {}",
+                  ADSP::OpusDecoder::Message::GetWorkBufferSizeForMultiStreamOK, msg);
+        return 0;
+    }
+    return shared_memory.dsp_return_data[0];
+}
+
+Result HardwareOpus::InitializeDecodeObject(u32 sample_rate, u32 channel_count, void* buffer,
+                                            u64 buffer_size) {
+    std::scoped_lock l{mutex};
+    shared_memory.host_send_data[0] = (u64)buffer;
+    shared_memory.host_send_data[1] = buffer_size;
+    shared_memory.host_send_data[2] = sample_rate;
+    shared_memory.host_send_data[3] = channel_count;
+
+    opus_decoder.Send(ADSP::Direction::DSP, ADSP::OpusDecoder::Message::InitializeDecodeObject);
+    auto msg = opus_decoder.Receive(ADSP::Direction::Host);
+    if (msg != ADSP::OpusDecoder::Message::InitializeDecodeObjectOK) {
+        LOG_ERROR(Service_Audio, "OpusDecoder returned invalid message. Expected {} got {}",
+                  ADSP::OpusDecoder::Message::InitializeDecodeObjectOK, msg);
+        R_THROW(ResultInvalidOpusDSPReturnCode);
+    }
+
+    R_RETURN(ResultCodeFromLibOpusErrorCode(shared_memory.dsp_return_data[0]));
+}
+
+Result HardwareOpus::InitializeMultiStreamDecodeObject(u32 sample_rate, u32 channel_count,
+                                                       u32 total_stream_count,
+                                                       u32 stereo_stream_count, void* mappings,
+                                                       void* buffer, u64 buffer_size) {
+    std::scoped_lock l{mutex};
+    shared_memory.host_send_data[0] = (u64)buffer;
+    shared_memory.host_send_data[1] = buffer_size;
+    shared_memory.host_send_data[2] = sample_rate;
+    shared_memory.host_send_data[3] = channel_count;
+    shared_memory.host_send_data[4] = total_stream_count;
+    shared_memory.host_send_data[5] = stereo_stream_count;
+
+    ASSERT(channel_count <= MaxChannels);
+    std::memcpy(shared_memory.channel_mapping.data(), mappings, channel_count * sizeof(u8));
+
+    opus_decoder.Send(ADSP::Direction::DSP,
+                      ADSP::OpusDecoder::Message::InitializeMultiStreamDecodeObject);
+    auto msg = opus_decoder.Receive(ADSP::Direction::Host);
+    if (msg != ADSP::OpusDecoder::Message::InitializeMultiStreamDecodeObjectOK) {
+        LOG_ERROR(Service_Audio, "OpusDecoder returned invalid message. Expected {} got {}",
+                  ADSP::OpusDecoder::Message::InitializeMultiStreamDecodeObjectOK, msg);
+        R_THROW(ResultInvalidOpusDSPReturnCode);
+    }
+
+    R_RETURN(ResultCodeFromLibOpusErrorCode(shared_memory.dsp_return_data[0]));
+}
+
+Result HardwareOpus::ShutdownDecodeObject(void* buffer, u64 buffer_size) {
+    std::scoped_lock l{mutex};
+    shared_memory.host_send_data[0] = (u64)buffer;
+    shared_memory.host_send_data[1] = buffer_size;
+
+    opus_decoder.Send(ADSP::Direction::DSP, ADSP::OpusDecoder::Message::ShutdownDecodeObject);
+    auto msg = opus_decoder.Receive(ADSP::Direction::Host);
+    ASSERT_MSG(msg == ADSP::OpusDecoder::Message::ShutdownDecodeObjectOK,
+               "Expected Opus shutdown code {}, got {}",
+               ADSP::OpusDecoder::Message::ShutdownDecodeObjectOK, msg);
+
+    R_RETURN(ResultCodeFromLibOpusErrorCode(shared_memory.dsp_return_data[0]));
+}
+
+Result HardwareOpus::ShutdownMultiStreamDecodeObject(void* buffer, u64 buffer_size) {
+    std::scoped_lock l{mutex};
+    shared_memory.host_send_data[0] = (u64)buffer;
+    shared_memory.host_send_data[1] = buffer_size;
+
+    opus_decoder.Send(ADSP::Direction::DSP,
+                      ADSP::OpusDecoder::Message::ShutdownMultiStreamDecodeObject);
+    auto msg = opus_decoder.Receive(ADSP::Direction::Host);
+    ASSERT_MSG(msg == ADSP::OpusDecoder::Message::ShutdownMultiStreamDecodeObjectOK,
+               "Expected Opus shutdown code {}, got {}",
+               ADSP::OpusDecoder::Message::ShutdownMultiStreamDecodeObjectOK, msg);
+
+    R_RETURN(ResultCodeFromLibOpusErrorCode(shared_memory.dsp_return_data[0]));
+}
+
+Result HardwareOpus::DecodeInterleaved(u32& out_sample_count, void* output_data,
+                                       u64 output_data_size, u32 channel_count, void* input_data,
+                                       u64 input_data_size, void* buffer, u64& out_time_taken,
+                                       bool reset) {
+    std::scoped_lock l{mutex};
+    shared_memory.host_send_data[0] = (u64)buffer;
+    shared_memory.host_send_data[1] = (u64)input_data;
+    shared_memory.host_send_data[2] = input_data_size;
+    shared_memory.host_send_data[3] = (u64)output_data;
+    shared_memory.host_send_data[4] = output_data_size;
+    shared_memory.host_send_data[5] = 0;
+    shared_memory.host_send_data[6] = reset;
+
+    opus_decoder.Send(ADSP::Direction::DSP, ADSP::OpusDecoder::Message::DecodeInterleaved);
+    auto msg = opus_decoder.Receive(ADSP::Direction::Host);
+    if (msg != ADSP::OpusDecoder::Message::DecodeInterleavedOK) {
+        LOG_ERROR(Service_Audio, "OpusDecoder returned invalid message. Expected {} got {}",
+                  ADSP::OpusDecoder::Message::DecodeInterleavedOK, msg);
+        R_THROW(ResultInvalidOpusDSPReturnCode);
+    }
+
+    auto error_code{static_cast<s32>(shared_memory.dsp_return_data[0])};
+    if (error_code == OPUS_OK) {
+        out_sample_count = static_cast<u32>(shared_memory.dsp_return_data[1]);
+        out_time_taken = 1000 * shared_memory.dsp_return_data[2];
+    }
+    R_RETURN(ResultCodeFromLibOpusErrorCode(error_code));
+}
+
+Result HardwareOpus::DecodeInterleavedForMultiStream(u32& out_sample_count, void* output_data,
+                                                     u64 output_data_size, u32 channel_count,
+                                                     void* input_data, u64 input_data_size,
+                                                     void* buffer, u64& out_time_taken,
+                                                     bool reset) {
+    std::scoped_lock l{mutex};
+    shared_memory.host_send_data[0] = (u64)buffer;
+    shared_memory.host_send_data[1] = (u64)input_data;
+    shared_memory.host_send_data[2] = input_data_size;
+    shared_memory.host_send_data[3] = (u64)output_data;
+    shared_memory.host_send_data[4] = output_data_size;
+    shared_memory.host_send_data[5] = 0;
+    shared_memory.host_send_data[6] = reset;
+
+    opus_decoder.Send(ADSP::Direction::DSP,
+                      ADSP::OpusDecoder::Message::DecodeInterleavedForMultiStream);
+    auto msg = opus_decoder.Receive(ADSP::Direction::Host);
+    if (msg != ADSP::OpusDecoder::Message::DecodeInterleavedForMultiStreamOK) {
+        LOG_ERROR(Service_Audio, "OpusDecoder returned invalid message. Expected {} got {}",
+                  ADSP::OpusDecoder::Message::DecodeInterleavedForMultiStreamOK, msg);
+        R_THROW(ResultInvalidOpusDSPReturnCode);
+    }
+
+    auto error_code{static_cast<s32>(shared_memory.dsp_return_data[0])};
+    if (error_code == OPUS_OK) {
+        out_sample_count = static_cast<u32>(shared_memory.dsp_return_data[1]);
+        out_time_taken = 1000 * shared_memory.dsp_return_data[2];
+    }
+    R_RETURN(ResultCodeFromLibOpusErrorCode(error_code));
+}
+
+Result HardwareOpus::MapMemory(void* buffer, u64 buffer_size) {
+    std::scoped_lock l{mutex};
+    shared_memory.host_send_data[0] = (u64)buffer;
+    shared_memory.host_send_data[1] = buffer_size;
+
+    opus_decoder.Send(ADSP::Direction::DSP, ADSP::OpusDecoder::Message::MapMemory);
+    auto msg = opus_decoder.Receive(ADSP::Direction::Host);
+    if (msg != ADSP::OpusDecoder::Message::MapMemoryOK) {
+        LOG_ERROR(Service_Audio, "OpusDecoder returned invalid message. Expected {} got {}",
+                  ADSP::OpusDecoder::Message::MapMemoryOK, msg);
+        R_THROW(ResultInvalidOpusDSPReturnCode);
+    }
+    R_SUCCEED();
+}
+
+Result HardwareOpus::UnmapMemory(void* buffer, u64 buffer_size) {
+    std::scoped_lock l{mutex};
+    shared_memory.host_send_data[0] = (u64)buffer;
+    shared_memory.host_send_data[1] = buffer_size;
+
+    opus_decoder.Send(ADSP::Direction::DSP, ADSP::OpusDecoder::Message::UnmapMemory);
+    auto msg = opus_decoder.Receive(ADSP::Direction::Host);
+    if (msg != ADSP::OpusDecoder::Message::UnmapMemoryOK) {
+        LOG_ERROR(Service_Audio, "OpusDecoder returned invalid message. Expected {} got {}",
+                  ADSP::OpusDecoder::Message::UnmapMemoryOK, msg);
+        R_THROW(ResultInvalidOpusDSPReturnCode);
+    }
+    R_SUCCEED();
+}
+
+} // namespace AudioCore::OpusDecoder
diff --git a/src/audio_core/opus/hardware_opus.h b/src/audio_core/opus/hardware_opus.h
new file mode 100644
index 0000000000..7013a6b404
--- /dev/null
+++ b/src/audio_core/opus/hardware_opus.h
@@ -0,0 +1,45 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include <mutex>
+#include <opus.h>
+
+#include "audio_core/adsp/apps/opus/opus_decoder.h"
+#include "audio_core/adsp/apps/opus/shared_memory.h"
+#include "audio_core/adsp/mailbox.h"
+#include "core/hle/service/audio/errors.h"
+
+namespace AudioCore::OpusDecoder {
+class HardwareOpus {
+public:
+    HardwareOpus(Core::System& system);
+
+    u64 GetWorkBufferSize(u32 channel);
+    u64 GetWorkBufferSizeForMultiStream(u32 total_stream_count, u32 stereo_stream_count);
+
+    Result InitializeDecodeObject(u32 sample_rate, u32 channel_count, void* buffer,
+                                  u64 buffer_size);
+    Result InitializeMultiStreamDecodeObject(u32 sample_rate, u32 channel_count,
+                                             u32 totaL_stream_count, u32 stereo_stream_count,
+                                             void* mappings, void* buffer, u64 buffer_size);
+    Result ShutdownDecodeObject(void* buffer, u64 buffer_size);
+    Result ShutdownMultiStreamDecodeObject(void* buffer, u64 buffer_size);
+    Result DecodeInterleaved(u32& out_sample_count, void* output_data, u64 output_data_size,
+                             u32 channel_count, void* input_data, u64 input_data_size, void* buffer,
+                             u64& out_time_taken, bool reset);
+    Result DecodeInterleavedForMultiStream(u32& out_sample_count, void* output_data,
+                                           u64 output_data_size, u32 channel_count,
+                                           void* input_data, u64 input_data_size, void* buffer,
+                                           u64& out_time_taken, bool reset);
+    Result MapMemory(void* buffer, u64 buffer_size);
+    Result UnmapMemory(void* buffer, u64 buffer_size);
+
+private:
+    Core::System& system;
+    std::mutex mutex;
+    ADSP::OpusDecoder::OpusDecoder& opus_decoder;
+    ADSP::OpusDecoder::SharedMemory shared_memory;
+};
+} // namespace AudioCore::OpusDecoder
diff --git a/src/audio_core/opus/parameters.h b/src/audio_core/opus/parameters.h
new file mode 100644
index 0000000000..4c54b28250
--- /dev/null
+++ b/src/audio_core/opus/parameters.h
@@ -0,0 +1,54 @@
+// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include "common/common_funcs.h"
+#include "common/common_types.h"
+
+namespace AudioCore::OpusDecoder {
+constexpr size_t OpusStreamCountMax = 255;
+constexpr size_t MaxChannels = 2;
+
+struct OpusParameters {
+    /* 0x00 */ u32 sample_rate;
+    /* 0x04 */ u32 channel_count;
+}; // size = 0x8
+static_assert(sizeof(OpusParameters) == 0x8, "OpusParameters has the wrong size!");
+
+struct OpusParametersEx {
+    /* 0x00 */ u32 sample_rate;
+    /* 0x04 */ u32 channel_count;
+    /* 0x08 */ bool use_large_frame_size;
+    /* 0x09 */ INSERT_PADDING_BYTES(7);
+}; // size = 0x10
+static_assert(sizeof(OpusParametersEx) == 0x10, "OpusParametersEx has the wrong size!");
+
+struct OpusMultiStreamParameters {
+    /* 0x00 */ u32 sample_rate;
+    /* 0x04 */ u32 channel_count;
+    /* 0x08 */ u32 total_stream_count;
+    /* 0x0C */ u32 stereo_stream_count;
+    /* 0x10 */ std::array<u8, OpusStreamCountMax + 1> mappings;
+}; // size = 0x110
+static_assert(sizeof(OpusMultiStreamParameters) == 0x110,
+              "OpusMultiStreamParameters has the wrong size!");
+
+struct OpusMultiStreamParametersEx {
+    /* 0x00 */ u32 sample_rate;
+    /* 0x04 */ u32 channel_count;
+    /* 0x08 */ u32 total_stream_count;
+    /* 0x0C */ u32 stereo_stream_count;
+    /* 0x10 */ bool use_large_frame_size;
+    /* 0x11 */ INSERT_PADDING_BYTES(7);
+    /* 0x18 */ std::array<u8, OpusStreamCountMax + 1> mappings;
+}; // size = 0x118
+static_assert(sizeof(OpusMultiStreamParametersEx) == 0x118,
+              "OpusMultiStreamParametersEx has the wrong size!");
+
+struct OpusPacketHeader {
+    /* 0x00 */ u32 size;
+    /* 0x04 */ u32 final_range;
+}; // size = 0x8
+static_assert(sizeof(OpusPacketHeader) == 0x8, "OpusPacketHeader has the wrong size!");
+} // namespace AudioCore::OpusDecoder
diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt
index 34877b461a..416203c598 100644
--- a/src/common/CMakeLists.txt
+++ b/src/common/CMakeLists.txt
@@ -26,12 +26,11 @@ add_library(common STATIC
     assert.h
     atomic_helpers.h
     atomic_ops.h
-    detached_tasks.cpp
-    detached_tasks.h
     bit_cast.h
     bit_field.h
     bit_set.h
     bit_util.h
+    bounded_threadsafe_queue.h
     cityhash.cpp
     cityhash.h
     common_funcs.h
@@ -41,6 +40,8 @@ add_library(common STATIC
     container_hash.h
     demangle.cpp
     demangle.h
+    detached_tasks.cpp
+    detached_tasks.h
     div_ceil.h
     dynamic_library.cpp
     dynamic_library.h
diff --git a/src/common/bounded_threadsafe_queue.h b/src/common/bounded_threadsafe_queue.h
index bd87aa09b5..b36fc1de96 100644
--- a/src/common/bounded_threadsafe_queue.h
+++ b/src/common/bounded_threadsafe_queue.h
@@ -45,13 +45,13 @@ public:
     }
 
     T PopWait() {
-        T t;
+        T t{};
         Pop<PopMode::Wait>(t);
         return t;
     }
 
     T PopWait(std::stop_token stop_token) {
-        T t;
+        T t{};
         Pop<PopMode::WaitWithStopToken>(t, stop_token);
         return t;
     }
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 30d2f7df64..b2dc71d4cb 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -890,7 +890,7 @@ endif()
 create_target_directory_groups(core)
 
 target_link_libraries(core PUBLIC common PRIVATE audio_core network video_core nx_tzdb)
-target_link_libraries(core PUBLIC Boost::headers PRIVATE fmt::fmt nlohmann_json::nlohmann_json mbedtls Opus::opus renderdoc)
+target_link_libraries(core PUBLIC Boost::headers PRIVATE fmt::fmt nlohmann_json::nlohmann_json mbedtls renderdoc)
 if (MINGW)
     target_link_libraries(core PRIVATE ${MSWSOCK_LIBRARY})
 endif()
diff --git a/src/core/hle/result.h b/src/core/hle/result.h
index 92a1439eb4..dd0b27f479 100644
--- a/src/core/hle/result.h
+++ b/src/core/hle/result.h
@@ -62,7 +62,7 @@ enum class ErrorModule : u32 {
     XCD = 108,
     TMP451 = 109,
     NIFM = 110,
-    Hwopus = 111,
+    HwOpus = 111,
     LSM6DS3 = 112,
     Bluetooth = 113,
     VI = 114,
diff --git a/src/core/hle/service/audio/errors.h b/src/core/hle/service/audio/errors.h
index 3d3d3d97a3..c41345f7e7 100644
--- a/src/core/hle/service/audio/errors.h
+++ b/src/core/hle/service/audio/errors.h
@@ -20,4 +20,16 @@ constexpr Result ResultNotSupported{ErrorModule::Audio, 513};
 constexpr Result ResultInvalidHandle{ErrorModule::Audio, 1536};
 constexpr Result ResultInvalidRevision{ErrorModule::Audio, 1537};
 
+constexpr Result ResultLibOpusAllocFail{ErrorModule::HwOpus, 7};
+constexpr Result ResultInputDataTooSmall{ErrorModule::HwOpus, 8};
+constexpr Result ResultLibOpusInvalidState{ErrorModule::HwOpus, 6};
+constexpr Result ResultLibOpusUnimplemented{ErrorModule::HwOpus, 5};
+constexpr Result ResultLibOpusInvalidPacket{ErrorModule::HwOpus, 17};
+constexpr Result ResultLibOpusInternalError{ErrorModule::HwOpus, 4};
+constexpr Result ResultBufferTooSmall{ErrorModule::HwOpus, 3};
+constexpr Result ResultLibOpusBadArg{ErrorModule::HwOpus, 2};
+constexpr Result ResultInvalidOpusDSPReturnCode{ErrorModule::HwOpus, 259};
+constexpr Result ResultInvalidOpusSampleRate{ErrorModule::HwOpus, 1001};
+constexpr Result ResultInvalidOpusChannelCount{ErrorModule::HwOpus, 1002};
+
 } // namespace Service::Audio
diff --git a/src/core/hle/service/audio/hwopus.cpp b/src/core/hle/service/audio/hwopus.cpp
index 1557e60885..6a7bf94162 100644
--- a/src/core/hle/service/audio/hwopus.cpp
+++ b/src/core/hle/service/audio/hwopus.cpp
@@ -1,420 +1,506 @@
 // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
 // SPDX-License-Identifier: GPL-2.0-or-later
 
-#include <chrono>
-#include <cstring>
 #include <memory>
 #include <vector>
 
-#include <opus.h>
-#include <opus_multistream.h>
-
+#include "audio_core/opus/decoder.h"
+#include "audio_core/opus/parameters.h"
 #include "common/assert.h"
 #include "common/logging/log.h"
 #include "common/scratch_buffer.h"
+#include "core/core.h"
 #include "core/hle/service/audio/hwopus.h"
 #include "core/hle/service/ipc_helpers.h"
 
 namespace Service::Audio {
-namespace {
-struct OpusDeleter {
-    void operator()(OpusMSDecoder* ptr) const {
-        opus_multistream_decoder_destroy(ptr);
-    }
-};
+using namespace AudioCore::OpusDecoder;
 
-using OpusDecoderPtr = std::unique_ptr<OpusMSDecoder, OpusDeleter>;
-
-struct OpusPacketHeader {
-    // Packet size in bytes.
-    u32_be size;
-    // Indicates the final range of the codec's entropy coder.
-    u32_be final_range;
-};
-static_assert(sizeof(OpusPacketHeader) == 0x8, "OpusHeader is an invalid size");
-
-class OpusDecoderState {
+class IHardwareOpusDecoder final : public ServiceFramework<IHardwareOpusDecoder> {
 public:
-    /// Describes extra behavior that may be asked of the decoding context.
-    enum class ExtraBehavior {
-        /// No extra behavior.
-        None,
-
-        /// Resets the decoder context back to a freshly initialized state.
-        ResetContext,
-    };
-
-    enum class PerfTime {
-        Disabled,
-        Enabled,
-    };
-
-    explicit OpusDecoderState(OpusDecoderPtr decoder_, u32 sample_rate_, u32 channel_count_)
-        : decoder{std::move(decoder_)}, sample_rate{sample_rate_}, channel_count{channel_count_} {}
-
-    // Decodes interleaved Opus packets. Optionally allows reporting time taken to
-    // perform the decoding, as well as any relevant extra behavior.
-    void DecodeInterleaved(HLERequestContext& ctx, PerfTime perf_time,
-                           ExtraBehavior extra_behavior) {
-        if (perf_time == PerfTime::Disabled) {
-            DecodeInterleavedHelper(ctx, nullptr, extra_behavior);
-        } else {
-            u64 performance = 0;
-            DecodeInterleavedHelper(ctx, &performance, extra_behavior);
-        }
-    }
-
-private:
-    void DecodeInterleavedHelper(HLERequestContext& ctx, u64* performance,
-                                 ExtraBehavior extra_behavior) {
-        u32 consumed = 0;
-        u32 sample_count = 0;
-        samples.resize_destructive(ctx.GetWriteBufferNumElements<opus_int16>());
-
-        if (extra_behavior == ExtraBehavior::ResetContext) {
-            ResetDecoderContext();
-        }
-
-        if (!DecodeOpusData(consumed, sample_count, ctx.ReadBuffer(), samples, performance)) {
-            LOG_ERROR(Audio, "Failed to decode opus data");
-            IPC::ResponseBuilder rb{ctx, 2};
-            // TODO(ogniK): Use correct error code
-            rb.Push(ResultUnknown);
-            return;
-        }
-
-        const u32 param_size = performance != nullptr ? 6 : 4;
-        IPC::ResponseBuilder rb{ctx, param_size};
-        rb.Push(ResultSuccess);
-        rb.Push<u32>(consumed);
-        rb.Push<u32>(sample_count);
-        if (performance) {
-            rb.Push<u64>(*performance);
-        }
-        ctx.WriteBuffer(samples);
-    }
-
-    bool DecodeOpusData(u32& consumed, u32& sample_count, std::span<const u8> input,
-                        std::span<opus_int16> output, u64* out_performance_time) const {
-        const auto start_time = std::chrono::steady_clock::now();
-        const std::size_t raw_output_sz = output.size() * sizeof(opus_int16);
-        if (sizeof(OpusPacketHeader) > input.size()) {
-            LOG_ERROR(Audio, "Input is smaller than the header size, header_sz={}, input_sz={}",
-                      sizeof(OpusPacketHeader), input.size());
-            return false;
-        }
-
-        OpusPacketHeader hdr{};
-        std::memcpy(&hdr, input.data(), sizeof(OpusPacketHeader));
-        if (sizeof(OpusPacketHeader) + static_cast<u32>(hdr.size) > input.size()) {
-            LOG_ERROR(Audio, "Input does not fit in the opus header size. data_sz={}, input_sz={}",
-                      sizeof(OpusPacketHeader) + static_cast<u32>(hdr.size), input.size());
-            return false;
-        }
-
-        const auto frame = input.data() + sizeof(OpusPacketHeader);
-        const auto decoded_sample_count = opus_packet_get_nb_samples(
-            frame, static_cast<opus_int32>(input.size() - sizeof(OpusPacketHeader)),
-            static_cast<opus_int32>(sample_rate));
-        if (decoded_sample_count * channel_count * sizeof(u16) > raw_output_sz) {
-            LOG_ERROR(
-                Audio,
-                "Decoded data does not fit into the output data, decoded_sz={}, raw_output_sz={}",
-                decoded_sample_count * channel_count * sizeof(u16), raw_output_sz);
-            return false;
-        }
-
-        const int frame_size = (static_cast<int>(raw_output_sz / sizeof(s16) / channel_count));
-        const auto out_sample_count =
-            opus_multistream_decode(decoder.get(), frame, hdr.size, output.data(), frame_size, 0);
-        if (out_sample_count < 0) {
-            LOG_ERROR(Audio,
-                      "Incorrect sample count received from opus_decode, "
-                      "output_sample_count={}, frame_size={}, data_sz_from_hdr={}",
-                      out_sample_count, frame_size, static_cast<u32>(hdr.size));
-            return false;
-        }
-
-        const auto end_time = std::chrono::steady_clock::now() - start_time;
-        sample_count = out_sample_count;
-        consumed = static_cast<u32>(sizeof(OpusPacketHeader) + hdr.size);
-        if (out_performance_time != nullptr) {
-            *out_performance_time =
-                std::chrono::duration_cast<std::chrono::milliseconds>(end_time).count();
-        }
-
-        return true;
-    }
-
-    void ResetDecoderContext() {
-        ASSERT(decoder != nullptr);
-
-        opus_multistream_decoder_ctl(decoder.get(), OPUS_RESET_STATE);
-    }
-
-    OpusDecoderPtr decoder;
-    u32 sample_rate;
-    u32 channel_count;
-    Common::ScratchBuffer<opus_int16> samples;
-};
-
-class IHardwareOpusDecoderManager final : public ServiceFramework<IHardwareOpusDecoderManager> {
-public:
-    explicit IHardwareOpusDecoderManager(Core::System& system_, OpusDecoderState decoder_state_)
-        : ServiceFramework{system_, "IHardwareOpusDecoderManager"}, decoder_state{
-                                                                        std::move(decoder_state_)} {
+    explicit IHardwareOpusDecoder(Core::System& system_, HardwareOpus& hardware_opus)
+        : ServiceFramework{system_, "IHardwareOpusDecoder"},
+          impl{std::make_unique<AudioCore::OpusDecoder::OpusDecoder>(system_, hardware_opus)} {
         // clang-format off
         static const FunctionInfo functions[] = {
-            {0, &IHardwareOpusDecoderManager::DecodeInterleavedOld, "DecodeInterleavedOld"},
-            {1, nullptr, "SetContext"},
-            {2, nullptr, "DecodeInterleavedForMultiStreamOld"},
-            {3, nullptr, "SetContextForMultiStream"},
-            {4, &IHardwareOpusDecoderManager::DecodeInterleavedWithPerfOld, "DecodeInterleavedWithPerfOld"},
-            {5, nullptr, "DecodeInterleavedForMultiStreamWithPerfOld"},
-            {6, &IHardwareOpusDecoderManager::DecodeInterleaved, "DecodeInterleavedWithPerfAndResetOld"},
-            {7, nullptr, "DecodeInterleavedForMultiStreamWithPerfAndResetOld"},
-            {8, &IHardwareOpusDecoderManager::DecodeInterleaved, "DecodeInterleaved"},
-            {9, &IHardwareOpusDecoderManager::DecodeInterleavedForMultiStream, "DecodeInterleavedForMultiStream"},
+            {0, &IHardwareOpusDecoder::DecodeInterleavedOld, "DecodeInterleavedOld"},
+            {1, &IHardwareOpusDecoder::SetContext, "SetContext"},
+            {2, &IHardwareOpusDecoder::DecodeInterleavedForMultiStreamOld, "DecodeInterleavedForMultiStreamOld"},
+            {3, &IHardwareOpusDecoder::SetContextForMultiStream, "SetContextForMultiStream"},
+            {4, &IHardwareOpusDecoder::DecodeInterleavedWithPerfOld, "DecodeInterleavedWithPerfOld"},
+            {5, &IHardwareOpusDecoder::DecodeInterleavedForMultiStreamWithPerfOld, "DecodeInterleavedForMultiStreamWithPerfOld"},
+            {6, &IHardwareOpusDecoder::DecodeInterleavedWithPerfAndResetOld, "DecodeInterleavedWithPerfAndResetOld"},
+            {7, &IHardwareOpusDecoder::DecodeInterleavedForMultiStreamWithPerfAndResetOld, "DecodeInterleavedForMultiStreamWithPerfAndResetOld"},
+            {8, &IHardwareOpusDecoder::DecodeInterleaved, "DecodeInterleaved"},
+            {9, &IHardwareOpusDecoder::DecodeInterleavedForMultiStream, "DecodeInterleavedForMultiStream"},
         };
         // clang-format on
 
         RegisterHandlers(functions);
     }
 
+    Result Initialize(OpusParametersEx& params, Kernel::KTransferMemory* transfer_memory,
+                      u64 transfer_memory_size) {
+        return impl->Initialize(params, transfer_memory, transfer_memory_size);
+    }
+
+    Result Initialize(OpusMultiStreamParametersEx& params, Kernel::KTransferMemory* transfer_memory,
+                      u64 transfer_memory_size) {
+        return impl->Initialize(params, transfer_memory, transfer_memory_size);
+    }
+
 private:
     void DecodeInterleavedOld(HLERequestContext& ctx) {
-        LOG_DEBUG(Audio, "called");
+        IPC::RequestParser rp{ctx};
 
-        decoder_state.DecodeInterleaved(ctx, OpusDecoderState::PerfTime::Disabled,
-                                        OpusDecoderState::ExtraBehavior::None);
+        auto input_data{ctx.ReadBuffer(0)};
+        output_data.resize_destructive(ctx.GetWriteBufferSize());
+
+        u32 size{};
+        u32 sample_count{};
+        auto result =
+            impl->DecodeInterleaved(&size, nullptr, &sample_count, input_data, output_data, false);
+
+        LOG_DEBUG(Service_Audio, "bytes read 0x{:X} samples generated {}", size, sample_count);
+
+        ctx.WriteBuffer(output_data);
+
+        IPC::ResponseBuilder rb{ctx, 4};
+        rb.Push(result);
+        rb.Push(size);
+        rb.Push(sample_count);
+    }
+
+    void SetContext(HLERequestContext& ctx) {
+        IPC::RequestParser rp{ctx};
+
+        LOG_DEBUG(Service_Audio, "called");
+
+        auto input_data{ctx.ReadBuffer(0)};
+        auto result = impl->SetContext(input_data);
+
+        IPC::ResponseBuilder rb{ctx, 2};
+        rb.Push(result);
+    }
+
+    void DecodeInterleavedForMultiStreamOld(HLERequestContext& ctx) {
+        IPC::RequestParser rp{ctx};
+
+        auto input_data{ctx.ReadBuffer(0)};
+        output_data.resize_destructive(ctx.GetWriteBufferSize());
+
+        u32 size{};
+        u32 sample_count{};
+        auto result = impl->DecodeInterleavedForMultiStream(&size, nullptr, &sample_count,
+                                                            input_data, output_data, false);
+
+        LOG_DEBUG(Service_Audio, "bytes read 0x{:X} samples generated {}", size, sample_count);
+
+        ctx.WriteBuffer(output_data);
+
+        IPC::ResponseBuilder rb{ctx, 4};
+        rb.Push(result);
+        rb.Push(size);
+        rb.Push(sample_count);
+    }
+
+    void SetContextForMultiStream(HLERequestContext& ctx) {
+        IPC::RequestParser rp{ctx};
+
+        LOG_DEBUG(Service_Audio, "called");
+
+        auto input_data{ctx.ReadBuffer(0)};
+        auto result = impl->SetContext(input_data);
+
+        IPC::ResponseBuilder rb{ctx, 2};
+        rb.Push(result);
     }
 
     void DecodeInterleavedWithPerfOld(HLERequestContext& ctx) {
-        LOG_DEBUG(Audio, "called");
+        IPC::RequestParser rp{ctx};
 
-        decoder_state.DecodeInterleaved(ctx, OpusDecoderState::PerfTime::Enabled,
-                                        OpusDecoderState::ExtraBehavior::None);
+        auto input_data{ctx.ReadBuffer(0)};
+        output_data.resize_destructive(ctx.GetWriteBufferSize());
+
+        u32 size{};
+        u32 sample_count{};
+        u64 time_taken{};
+        auto result = impl->DecodeInterleaved(&size, &time_taken, &sample_count, input_data,
+                                              output_data, false);
+
+        LOG_DEBUG(Service_Audio, "bytes read 0x{:X} samples generated {} time taken {}", size,
+                  sample_count, time_taken);
+
+        ctx.WriteBuffer(output_data);
+
+        IPC::ResponseBuilder rb{ctx, 6};
+        rb.Push(result);
+        rb.Push(size);
+        rb.Push(sample_count);
+        rb.Push(time_taken);
+    }
+
+    void DecodeInterleavedForMultiStreamWithPerfOld(HLERequestContext& ctx) {
+        IPC::RequestParser rp{ctx};
+
+        auto input_data{ctx.ReadBuffer(0)};
+        output_data.resize_destructive(ctx.GetWriteBufferSize());
+
+        u32 size{};
+        u32 sample_count{};
+        u64 time_taken{};
+        auto result = impl->DecodeInterleavedForMultiStream(&size, &time_taken, &sample_count,
+                                                            input_data, output_data, false);
+
+        LOG_DEBUG(Service_Audio, "bytes read 0x{:X} samples generated {} time taken {}", size,
+                  sample_count, time_taken);
+
+        ctx.WriteBuffer(output_data);
+
+        IPC::ResponseBuilder rb{ctx, 6};
+        rb.Push(result);
+        rb.Push(size);
+        rb.Push(sample_count);
+        rb.Push(time_taken);
+    }
+
+    void DecodeInterleavedWithPerfAndResetOld(HLERequestContext& ctx) {
+        IPC::RequestParser rp{ctx};
+
+        auto reset{rp.Pop<bool>()};
+
+        auto input_data{ctx.ReadBuffer(0)};
+        output_data.resize_destructive(ctx.GetWriteBufferSize());
+
+        u32 size{};
+        u32 sample_count{};
+        u64 time_taken{};
+        auto result = impl->DecodeInterleaved(&size, &time_taken, &sample_count, input_data,
+                                              output_data, reset);
+
+        LOG_DEBUG(Service_Audio, "reset {} bytes read 0x{:X} samples generated {} time taken {}",
+                  reset, size, sample_count, time_taken);
+
+        ctx.WriteBuffer(output_data);
+
+        IPC::ResponseBuilder rb{ctx, 6};
+        rb.Push(result);
+        rb.Push(size);
+        rb.Push(sample_count);
+        rb.Push(time_taken);
+    }
+
+    void DecodeInterleavedForMultiStreamWithPerfAndResetOld(HLERequestContext& ctx) {
+        IPC::RequestParser rp{ctx};
+
+        auto reset{rp.Pop<bool>()};
+
+        auto input_data{ctx.ReadBuffer(0)};
+        output_data.resize_destructive(ctx.GetWriteBufferSize());
+
+        u32 size{};
+        u32 sample_count{};
+        u64 time_taken{};
+        auto result = impl->DecodeInterleavedForMultiStream(&size, &time_taken, &sample_count,
+                                                            input_data, output_data, reset);
+
+        LOG_DEBUG(Service_Audio, "reset {} bytes read 0x{:X} samples generated {} time taken {}",
+                  reset, size, sample_count, time_taken);
+
+        ctx.WriteBuffer(output_data);
+
+        IPC::ResponseBuilder rb{ctx, 6};
+        rb.Push(result);
+        rb.Push(size);
+        rb.Push(sample_count);
+        rb.Push(time_taken);
     }
 
     void DecodeInterleaved(HLERequestContext& ctx) {
-        LOG_DEBUG(Audio, "called");
-
         IPC::RequestParser rp{ctx};
-        const auto extra_behavior = rp.Pop<bool>() ? OpusDecoderState::ExtraBehavior::ResetContext
-                                                   : OpusDecoderState::ExtraBehavior::None;
 
-        decoder_state.DecodeInterleaved(ctx, OpusDecoderState::PerfTime::Enabled, extra_behavior);
+        auto reset{rp.Pop<bool>()};
+
+        auto input_data{ctx.ReadBuffer(0)};
+        output_data.resize_destructive(ctx.GetWriteBufferSize());
+
+        u32 size{};
+        u32 sample_count{};
+        u64 time_taken{};
+        auto result = impl->DecodeInterleaved(&size, &time_taken, &sample_count, input_data,
+                                              output_data, reset);
+
+        LOG_DEBUG(Service_Audio, "reset {} bytes read 0x{:X} samples generated {} time taken {}",
+                  reset, size, sample_count, time_taken);
+
+        ctx.WriteBuffer(output_data);
+
+        IPC::ResponseBuilder rb{ctx, 6};
+        rb.Push(result);
+        rb.Push(size);
+        rb.Push(sample_count);
+        rb.Push(time_taken);
     }
 
     void DecodeInterleavedForMultiStream(HLERequestContext& ctx) {
-        LOG_DEBUG(Audio, "called");
-
         IPC::RequestParser rp{ctx};
-        const auto extra_behavior = rp.Pop<bool>() ? OpusDecoderState::ExtraBehavior::ResetContext
-                                                   : OpusDecoderState::ExtraBehavior::None;
 
-        decoder_state.DecodeInterleaved(ctx, OpusDecoderState::PerfTime::Enabled, extra_behavior);
+        auto reset{rp.Pop<bool>()};
+
+        auto input_data{ctx.ReadBuffer(0)};
+        output_data.resize_destructive(ctx.GetWriteBufferSize());
+
+        u32 size{};
+        u32 sample_count{};
+        u64 time_taken{};
+        auto result = impl->DecodeInterleavedForMultiStream(&size, &time_taken, &sample_count,
+                                                            input_data, output_data, reset);
+
+        LOG_DEBUG(Service_Audio, "reset {} bytes read 0x{:X} samples generated {} time taken {}",
+                  reset, size, sample_count, time_taken);
+
+        ctx.WriteBuffer(output_data);
+
+        IPC::ResponseBuilder rb{ctx, 6};
+        rb.Push(result);
+        rb.Push(size);
+        rb.Push(sample_count);
+        rb.Push(time_taken);
     }
 
-    OpusDecoderState decoder_state;
+    std::unique_ptr<AudioCore::OpusDecoder::OpusDecoder> impl;
+    Common::ScratchBuffer<u8> output_data;
 };
 
-std::size_t WorkerBufferSize(u32 channel_count) {
-    ASSERT_MSG(channel_count == 1 || channel_count == 2, "Invalid channel count");
-    constexpr int num_streams = 1;
-    const int num_stereo_streams = channel_count == 2 ? 1 : 0;
-    return opus_multistream_decoder_get_size(num_streams, num_stereo_streams);
-}
-
-// Creates the mapping table that maps the input channels to the particular
-// output channels. In the stereo case, we map the left and right input channels
-// to the left and right output channels respectively.
-//
-// However, in the monophonic case, we only map the one available channel
-// to the sole output channel. We specify 255 for the would-be right channel
-// as this is a special value defined by Opus to indicate to the decoder to
-// ignore that channel.
-std::array<u8, 2> CreateMappingTable(u32 channel_count) {
-    if (channel_count == 2) {
-        return {{0, 1}};
-    }
-
-    return {{0, 255}};
-}
-} // Anonymous namespace
-
-void HwOpus::GetWorkBufferSize(HLERequestContext& ctx) {
-    IPC::RequestParser rp{ctx};
-    const auto sample_rate = rp.Pop<u32>();
-    const auto channel_count = rp.Pop<u32>();
-
-    LOG_DEBUG(Audio, "called with 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,
-               "Invalid sample rate");
-    ASSERT_MSG(channel_count == 1 || channel_count == 2, "Invalid channel count");
-
-    const u32 worker_buffer_sz = static_cast<u32>(WorkerBufferSize(channel_count));
-    LOG_DEBUG(Audio, "worker_buffer_sz={}", worker_buffer_sz);
-
-    IPC::ResponseBuilder rb{ctx, 3};
-    rb.Push(ResultSuccess);
-    rb.Push<u32>(worker_buffer_sz);
-}
-
-void HwOpus::GetWorkBufferSizeEx(HLERequestContext& ctx) {
-    GetWorkBufferSize(ctx);
-}
-
-void HwOpus::GetWorkBufferSizeExEx(HLERequestContext& ctx) {
-    GetWorkBufferSizeEx(ctx);
-}
-
-void HwOpus::GetWorkBufferSizeForMultiStreamEx(HLERequestContext& ctx) {
-    OpusMultiStreamParametersEx param;
-    std::memcpy(&param, ctx.ReadBuffer().data(), ctx.GetReadBufferSize());
-
-    const auto sample_rate = param.sample_rate;
-    const auto channel_count = param.channel_count;
-    const auto number_streams = param.number_streams;
-    const auto number_stereo_streams = param.number_stereo_streams;
-
-    LOG_DEBUG(
-        Audio,
-        "called with sample_rate={}, channel_count={}, number_streams={}, number_stereo_streams={}",
-        sample_rate, channel_count, number_streams, number_stereo_streams);
-
-    ASSERT_MSG(sample_rate == 48000 || sample_rate == 24000 || sample_rate == 16000 ||
-                   sample_rate == 12000 || sample_rate == 8000,
-               "Invalid sample rate");
-
-    const u32 worker_buffer_sz =
-        static_cast<u32>(opus_multistream_decoder_get_size(number_streams, number_stereo_streams));
-
-    IPC::ResponseBuilder rb{ctx, 3};
-    rb.Push(ResultSuccess);
-    rb.Push<u32>(worker_buffer_sz);
-}
-
 void HwOpus::OpenHardwareOpusDecoder(HLERequestContext& ctx) {
     IPC::RequestParser rp{ctx};
-    const auto sample_rate = rp.Pop<u32>();
-    const auto channel_count = rp.Pop<u32>();
-    const auto buffer_sz = rp.Pop<u32>();
 
-    LOG_DEBUG(Audio, "called sample_rate={}, channel_count={}, buffer_size={}", sample_rate,
-              channel_count, buffer_sz);
+    auto params = rp.PopRaw<OpusParameters>();
+    auto transfer_memory_size{rp.Pop<u32>()};
+    auto transfer_memory_handle{ctx.GetCopyHandle(0)};
+    auto transfer_memory{
+        system.ApplicationProcess()->GetHandleTable().GetObject<Kernel::KTransferMemory>(
+            transfer_memory_handle)};
 
-    ASSERT_MSG(sample_rate == 48000 || sample_rate == 24000 || sample_rate == 16000 ||
-                   sample_rate == 12000 || sample_rate == 8000,
-               "Invalid sample rate");
-    ASSERT_MSG(channel_count == 1 || channel_count == 2, "Invalid channel count");
+    LOG_DEBUG(Service_Audio, "sample_rate {} channel_count {} transfer_memory_size 0x{:X}",
+              params.sample_rate, params.channel_count, transfer_memory_size);
 
-    const std::size_t worker_sz = WorkerBufferSize(channel_count);
-    ASSERT_MSG(buffer_sz >= worker_sz, "Worker buffer too large");
+    auto decoder{std::make_shared<IHardwareOpusDecoder>(system, impl.GetHardwareOpus())};
 
-    const int num_stereo_streams = channel_count == 2 ? 1 : 0;
-    const auto mapping_table = CreateMappingTable(channel_count);
-
-    int error = 0;
-    OpusDecoderPtr decoder{
-        opus_multistream_decoder_create(sample_rate, static_cast<int>(channel_count), 1,
-                                        num_stereo_streams, mapping_table.data(), &error)};
-    if (error != OPUS_OK || decoder == nullptr) {
-        LOG_ERROR(Audio, "Failed to create Opus decoder (error={}).", error);
-        IPC::ResponseBuilder rb{ctx, 2};
-        // TODO(ogniK): Use correct error code
-        rb.Push(ResultUnknown);
-        return;
-    }
+    OpusParametersEx ex{
+        .sample_rate = params.sample_rate,
+        .channel_count = params.channel_count,
+        .use_large_frame_size = false,
+    };
+    auto result = decoder->Initialize(ex, transfer_memory.GetPointerUnsafe(), transfer_memory_size);
 
     IPC::ResponseBuilder rb{ctx, 2, 0, 1};
-    rb.Push(ResultSuccess);
-    rb.PushIpcInterface<IHardwareOpusDecoderManager>(
-        system, OpusDecoderState{std::move(decoder), sample_rate, channel_count});
+    rb.Push(result);
+    rb.PushIpcInterface(decoder);
+}
+
+void HwOpus::GetWorkBufferSize(HLERequestContext& ctx) {
+    IPC::RequestParser rp{ctx};
+    auto params = rp.PopRaw<OpusParameters>();
+
+    u64 size{};
+    auto result = impl.GetWorkBufferSize(params, size);
+
+    LOG_DEBUG(Service_Audio, "sample_rate {} channel_count {} -- returned size 0x{:X}",
+              params.sample_rate, params.channel_count, size);
+
+    IPC::ResponseBuilder rb{ctx, 4};
+    rb.Push(result);
+    rb.Push(size);
+}
+
+void HwOpus::OpenHardwareOpusDecoderForMultiStream(HLERequestContext& ctx) {
+    IPC::RequestParser rp{ctx};
+
+    auto input{ctx.ReadBuffer()};
+    OpusMultiStreamParameters params;
+    std::memcpy(&params, input.data(), sizeof(OpusMultiStreamParameters));
+
+    auto transfer_memory_size{rp.Pop<u32>()};
+    auto transfer_memory_handle{ctx.GetCopyHandle(0)};
+    auto transfer_memory{
+        system.ApplicationProcess()->GetHandleTable().GetObject<Kernel::KTransferMemory>(
+            transfer_memory_handle)};
+
+    LOG_DEBUG(Service_Audio,
+              "sample_rate {} channel_count {} total_stream_count {} stereo_stream_count {} "
+              "transfer_memory_size 0x{:X}",
+              params.sample_rate, params.channel_count, params.total_stream_count,
+              params.stereo_stream_count, transfer_memory_size);
+
+    auto decoder{std::make_shared<IHardwareOpusDecoder>(system, impl.GetHardwareOpus())};
+
+    OpusMultiStreamParametersEx ex{
+        .sample_rate = params.sample_rate,
+        .channel_count = params.channel_count,
+        .total_stream_count = params.total_stream_count,
+        .stereo_stream_count = params.stereo_stream_count,
+        .use_large_frame_size = false,
+        .mappings{},
+    };
+    std::memcpy(ex.mappings.data(), params.mappings.data(), sizeof(params.mappings));
+    auto result = decoder->Initialize(ex, transfer_memory.GetPointerUnsafe(), transfer_memory_size);
+
+    IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+    rb.Push(result);
+    rb.PushIpcInterface(decoder);
+}
+
+void HwOpus::GetWorkBufferSizeForMultiStream(HLERequestContext& ctx) {
+    IPC::RequestParser rp{ctx};
+
+    auto input{ctx.ReadBuffer()};
+    OpusMultiStreamParameters params;
+    std::memcpy(&params, input.data(), sizeof(OpusMultiStreamParameters));
+
+    u64 size{};
+    auto result = impl.GetWorkBufferSizeForMultiStream(params, size);
+
+    LOG_DEBUG(Service_Audio, "size 0x{:X}", size);
+
+    IPC::ResponseBuilder rb{ctx, 4};
+    rb.Push(result);
+    rb.Push(size);
 }
 
 void HwOpus::OpenHardwareOpusDecoderEx(HLERequestContext& ctx) {
     IPC::RequestParser rp{ctx};
-    const auto sample_rate = rp.Pop<u32>();
-    const auto channel_count = rp.Pop<u32>();
 
-    LOG_DEBUG(Audio, "called sample_rate={}, channel_count={}", sample_rate, channel_count);
+    auto params = rp.PopRaw<OpusParametersEx>();
+    auto transfer_memory_size{rp.Pop<u32>()};
+    auto transfer_memory_handle{ctx.GetCopyHandle(0)};
+    auto transfer_memory{
+        system.ApplicationProcess()->GetHandleTable().GetObject<Kernel::KTransferMemory>(
+            transfer_memory_handle)};
 
-    ASSERT_MSG(sample_rate == 48000 || sample_rate == 24000 || sample_rate == 16000 ||
-                   sample_rate == 12000 || sample_rate == 8000,
-               "Invalid sample rate");
-    ASSERT_MSG(channel_count == 1 || channel_count == 2, "Invalid channel count");
+    LOG_DEBUG(Service_Audio, "sample_rate {} channel_count {} transfer_memory_size 0x{:X}",
+              params.sample_rate, params.channel_count, transfer_memory_size);
 
-    const int num_stereo_streams = channel_count == 2 ? 1 : 0;
-    const auto mapping_table = CreateMappingTable(channel_count);
+    auto decoder{std::make_shared<IHardwareOpusDecoder>(system, impl.GetHardwareOpus())};
 
-    int error = 0;
-    OpusDecoderPtr decoder{
-        opus_multistream_decoder_create(sample_rate, static_cast<int>(channel_count), 1,
-                                        num_stereo_streams, mapping_table.data(), &error)};
-    if (error != OPUS_OK || decoder == nullptr) {
-        LOG_ERROR(Audio, "Failed to create Opus decoder (error={}).", error);
-        IPC::ResponseBuilder rb{ctx, 2};
-        // TODO(ogniK): Use correct error code
-        rb.Push(ResultUnknown);
-        return;
-    }
+    auto result =
+        decoder->Initialize(params, transfer_memory.GetPointerUnsafe(), transfer_memory_size);
 
     IPC::ResponseBuilder rb{ctx, 2, 0, 1};
-    rb.Push(ResultSuccess);
-    rb.PushIpcInterface<IHardwareOpusDecoderManager>(
-        system, OpusDecoderState{std::move(decoder), sample_rate, channel_count});
+    rb.Push(result);
+    rb.PushIpcInterface(decoder);
+}
+
+void HwOpus::GetWorkBufferSizeEx(HLERequestContext& ctx) {
+    IPC::RequestParser rp{ctx};
+    auto params = rp.PopRaw<OpusParametersEx>();
+
+    u64 size{};
+    auto result = impl.GetWorkBufferSizeEx(params, size);
+
+    LOG_DEBUG(Service_Audio, "size 0x{:X}", size);
+
+    IPC::ResponseBuilder rb{ctx, 4};
+    rb.Push(result);
+    rb.Push(size);
 }
 
 void HwOpus::OpenHardwareOpusDecoderForMultiStreamEx(HLERequestContext& ctx) {
+    IPC::RequestParser rp{ctx};
+
+    auto input{ctx.ReadBuffer()};
     OpusMultiStreamParametersEx params;
-    std::memcpy(&params, ctx.ReadBuffer().data(), ctx.GetReadBufferSize());
+    std::memcpy(&params, input.data(), sizeof(OpusMultiStreamParametersEx));
 
-    const auto& sample_rate = params.sample_rate;
-    const auto& channel_count = params.channel_count;
+    auto transfer_memory_size{rp.Pop<u32>()};
+    auto transfer_memory_handle{ctx.GetCopyHandle(0)};
+    auto transfer_memory{
+        system.ApplicationProcess()->GetHandleTable().GetObject<Kernel::KTransferMemory>(
+            transfer_memory_handle)};
 
-    LOG_INFO(
-        Audio,
-        "called with sample_rate={}, channel_count={}, number_streams={}, number_stereo_streams={}",
-        sample_rate, channel_count, params.number_streams, params.number_stereo_streams);
+    LOG_DEBUG(Service_Audio,
+              "sample_rate {} channel_count {} total_stream_count {} stereo_stream_count {} "
+              "use_large_frame_size {}"
+              "transfer_memory_size 0x{:X}",
+              params.sample_rate, params.channel_count, params.total_stream_count,
+              params.stereo_stream_count, params.use_large_frame_size, transfer_memory_size);
 
-    ASSERT_MSG(sample_rate == 48000 || sample_rate == 24000 || sample_rate == 16000 ||
-                   sample_rate == 12000 || sample_rate == 8000,
-               "Invalid sample rate");
+    auto decoder{std::make_shared<IHardwareOpusDecoder>(system, impl.GetHardwareOpus())};
 
-    int error = 0;
-    OpusDecoderPtr decoder{opus_multistream_decoder_create(
-        sample_rate, static_cast<int>(channel_count), params.number_streams,
-        params.number_stereo_streams, params.channel_mappings.data(), &error)};
-    if (error != OPUS_OK || decoder == nullptr) {
-        LOG_ERROR(Audio, "Failed to create Opus decoder (error={}).", error);
-        IPC::ResponseBuilder rb{ctx, 2};
-        // TODO(ogniK): Use correct error code
-        rb.Push(ResultUnknown);
-        return;
-    }
+    auto result =
+        decoder->Initialize(params, transfer_memory.GetPointerUnsafe(), transfer_memory_size);
 
     IPC::ResponseBuilder rb{ctx, 2, 0, 1};
-    rb.Push(ResultSuccess);
-    rb.PushIpcInterface<IHardwareOpusDecoderManager>(
-        system, OpusDecoderState{std::move(decoder), sample_rate, channel_count});
+    rb.Push(result);
+    rb.PushIpcInterface(decoder);
 }
 
-HwOpus::HwOpus(Core::System& system_) : ServiceFramework{system_, "hwopus"} {
+void HwOpus::GetWorkBufferSizeForMultiStreamEx(HLERequestContext& ctx) {
+    IPC::RequestParser rp{ctx};
+
+    auto input{ctx.ReadBuffer()};
+    OpusMultiStreamParametersEx params;
+    std::memcpy(&params, input.data(), sizeof(OpusMultiStreamParametersEx));
+
+    u64 size{};
+    auto result = impl.GetWorkBufferSizeForMultiStreamEx(params, size);
+
+    LOG_DEBUG(Service_Audio,
+              "sample_rate {} channel_count {} total_stream_count {} stereo_stream_count {} "
+              "use_large_frame_size {} -- returned size 0x{:X}",
+              params.sample_rate, params.channel_count, params.total_stream_count,
+              params.stereo_stream_count, params.use_large_frame_size, size);
+
+    IPC::ResponseBuilder rb{ctx, 4};
+    rb.Push(result);
+    rb.Push(size);
+}
+
+void HwOpus::GetWorkBufferSizeExEx(HLERequestContext& ctx) {
+    IPC::RequestParser rp{ctx};
+    auto params = rp.PopRaw<OpusParametersEx>();
+
+    u64 size{};
+    auto result = impl.GetWorkBufferSizeExEx(params, size);
+
+    LOG_DEBUG(Service_Audio, "size 0x{:X}", size);
+
+    IPC::ResponseBuilder rb{ctx, 4};
+    rb.Push(result);
+    rb.Push(size);
+}
+
+void HwOpus::GetWorkBufferSizeForMultiStreamExEx(HLERequestContext& ctx) {
+    IPC::RequestParser rp{ctx};
+
+    auto input{ctx.ReadBuffer()};
+    OpusMultiStreamParametersEx params;
+    std::memcpy(&params, input.data(), sizeof(OpusMultiStreamParametersEx));
+
+    u64 size{};
+    auto result = impl.GetWorkBufferSizeForMultiStreamExEx(params, size);
+
+    LOG_DEBUG(Service_Audio, "size 0x{:X}", size);
+
+    IPC::ResponseBuilder rb{ctx, 4};
+    rb.Push(result);
+    rb.Push(size);
+}
+
+HwOpus::HwOpus(Core::System& system_)
+    : ServiceFramework{system_, "hwopus"}, system{system_}, impl{system} {
     static const FunctionInfo functions[] = {
         {0, &HwOpus::OpenHardwareOpusDecoder, "OpenHardwareOpusDecoder"},
         {1, &HwOpus::GetWorkBufferSize, "GetWorkBufferSize"},
-        {2, nullptr, "OpenOpusDecoderForMultiStream"},
-        {3, nullptr, "GetWorkBufferSizeForMultiStream"},
+        {2, &HwOpus::OpenHardwareOpusDecoderForMultiStream, "OpenOpusDecoderForMultiStream"},
+        {3, &HwOpus::GetWorkBufferSizeForMultiStream, "GetWorkBufferSizeForMultiStream"},
         {4, &HwOpus::OpenHardwareOpusDecoderEx, "OpenHardwareOpusDecoderEx"},
         {5, &HwOpus::GetWorkBufferSizeEx, "GetWorkBufferSizeEx"},
         {6, &HwOpus::OpenHardwareOpusDecoderForMultiStreamEx,
          "OpenHardwareOpusDecoderForMultiStreamEx"},
         {7, &HwOpus::GetWorkBufferSizeForMultiStreamEx, "GetWorkBufferSizeForMultiStreamEx"},
         {8, &HwOpus::GetWorkBufferSizeExEx, "GetWorkBufferSizeExEx"},
-        {9, nullptr, "GetWorkBufferSizeForMultiStreamExEx"},
+        {9, &HwOpus::GetWorkBufferSizeForMultiStreamExEx, "GetWorkBufferSizeForMultiStreamExEx"},
     };
     RegisterHandlers(functions);
 }
diff --git a/src/core/hle/service/audio/hwopus.h b/src/core/hle/service/audio/hwopus.h
index 90867bf74d..d3960065e7 100644
--- a/src/core/hle/service/audio/hwopus.h
+++ b/src/core/hle/service/audio/hwopus.h
@@ -3,6 +3,7 @@
 
 #pragma once
 
+#include "audio_core/opus/decoder_manager.h"
 #include "core/hle/service/service.h"
 
 namespace Core {
@@ -11,18 +12,6 @@ class System;
 
 namespace Service::Audio {
 
-struct OpusMultiStreamParametersEx {
-    u32 sample_rate;
-    u32 channel_count;
-    u32 number_streams;
-    u32 number_stereo_streams;
-    u32 use_large_frame_size;
-    u32 padding;
-    std::array<u8, 0x100> channel_mappings;
-};
-static_assert(sizeof(OpusMultiStreamParametersEx) == 0x118,
-              "OpusMultiStreamParametersEx has incorrect size");
-
 class HwOpus final : public ServiceFramework<HwOpus> {
 public:
     explicit HwOpus(Core::System& system_);
@@ -30,12 +19,18 @@ public:
 
 private:
     void OpenHardwareOpusDecoder(HLERequestContext& ctx);
-    void OpenHardwareOpusDecoderEx(HLERequestContext& ctx);
-    void OpenHardwareOpusDecoderForMultiStreamEx(HLERequestContext& ctx);
     void GetWorkBufferSize(HLERequestContext& ctx);
+    void OpenHardwareOpusDecoderForMultiStream(HLERequestContext& ctx);
+    void GetWorkBufferSizeForMultiStream(HLERequestContext& ctx);
+    void OpenHardwareOpusDecoderEx(HLERequestContext& ctx);
     void GetWorkBufferSizeEx(HLERequestContext& ctx);
-    void GetWorkBufferSizeExEx(HLERequestContext& ctx);
+    void OpenHardwareOpusDecoderForMultiStreamEx(HLERequestContext& ctx);
     void GetWorkBufferSizeForMultiStreamEx(HLERequestContext& ctx);
+    void GetWorkBufferSizeExEx(HLERequestContext& ctx);
+    void GetWorkBufferSizeForMultiStreamExEx(HLERequestContext& ctx);
+
+    Core::System& system;
+    AudioCore::OpusDecoder::OpusDecoderManager impl;
 };
 
 } // namespace Service::Audio