diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 85354f43e7..a88551fbc8 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -3,6 +3,7 @@ include_directories(.)
 
 add_subdirectory(common)
 add_subdirectory(core)
+add_subdirectory(audio_core)
 add_subdirectory(video_core)
 add_subdirectory(input_common)
 add_subdirectory(tests)
diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt
new file mode 100644
index 0000000000..f00a55994f
--- /dev/null
+++ b/src/audio_core/CMakeLists.txt
@@ -0,0 +1,11 @@
+add_library(audio_core STATIC
+    audio_out.cpp
+    audio_out.h
+    buffer.h
+    stream.cpp
+    stream.h
+)
+
+create_target_directory_groups(audio_core)
+
+target_link_libraries(audio_core PUBLIC common core)
diff --git a/src/audio_core/audio_out.cpp b/src/audio_core/audio_out.cpp
new file mode 100644
index 0000000000..6d418a05b1
--- /dev/null
+++ b/src/audio_core/audio_out.cpp
@@ -0,0 +1,50 @@
+// Copyright 2018 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "audio_core/audio_out.h"
+#include "common/assert.h"
+#include "common/logging/log.h"
+
+namespace AudioCore {
+
+/// Returns the stream format from the specified number of channels
+static Stream::Format ChannelsToStreamFormat(int num_channels) {
+    switch (num_channels) {
+    case 1:
+        return Stream::Format::Mono16;
+    case 2:
+        return Stream::Format::Stereo16;
+    case 6:
+        return Stream::Format::Multi51Channel16;
+    }
+
+    LOG_CRITICAL(Audio, "Unimplemented num_channels={}", num_channels);
+    UNREACHABLE();
+    return {};
+}
+
+StreamPtr AudioOut::OpenStream(int sample_rate, int num_channels,
+                               Stream::ReleaseCallback&& release_callback) {
+    streams.push_back(std::make_shared<Stream>(sample_rate, ChannelsToStreamFormat(num_channels),
+                                               std::move(release_callback)));
+    return streams.back();
+}
+
+std::vector<u64> AudioOut::GetTagsAndReleaseBuffers(StreamPtr stream, size_t max_count) {
+    return stream->GetTagsAndReleaseBuffers(max_count);
+}
+
+void AudioOut::StartStream(StreamPtr stream) {
+    stream->Play();
+}
+
+void AudioOut::StopStream(StreamPtr stream) {
+    stream->Stop();
+}
+
+bool AudioOut::QueueBuffer(StreamPtr stream, Buffer::Tag tag, std::vector<u8>&& data) {
+    return stream->QueueBuffer(std::make_shared<Buffer>(tag, std::move(data)));
+}
+
+} // namespace AudioCore
diff --git a/src/audio_core/audio_out.h b/src/audio_core/audio_out.h
new file mode 100644
index 0000000000..a86499d103
--- /dev/null
+++ b/src/audio_core/audio_out.h
@@ -0,0 +1,44 @@
+// Copyright 2018 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <memory>
+#include <vector>
+
+#include "audio_core/buffer.h"
+#include "audio_core/stream.h"
+#include "common/common_types.h"
+
+namespace AudioCore {
+
+using StreamPtr = std::shared_ptr<Stream>;
+
+/**
+ * Represents an audio playback interface, used to open and play audio streams
+ */
+class AudioOut {
+public:
+    /// Opens a new audio stream
+    StreamPtr OpenStream(int sample_rate, int num_channels,
+                         Stream::ReleaseCallback&& release_callback);
+
+    /// Returns a vector of recently released buffers specified by tag for the specified stream
+    std::vector<u64> GetTagsAndReleaseBuffers(StreamPtr stream, size_t max_count);
+
+    /// Starts an audio stream for playback
+    void StartStream(StreamPtr stream);
+
+    /// Stops an audio stream that is currently playing
+    void StopStream(StreamPtr stream);
+
+    /// Queues a buffer into the specified audio stream, returns true on success
+    bool QueueBuffer(StreamPtr stream, Buffer::Tag tag, std::vector<u8>&& data);
+
+private:
+    /// Active audio streams on the interface
+    std::vector<StreamPtr> streams;
+};
+
+} // namespace AudioCore
diff --git a/src/audio_core/buffer.h b/src/audio_core/buffer.h
new file mode 100644
index 0000000000..874ec787e2
--- /dev/null
+++ b/src/audio_core/buffer.h
@@ -0,0 +1,37 @@
+// Copyright 2018 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <vector>
+
+#include "common/common_types.h"
+
+namespace AudioCore {
+
+/**
+ * Represents a buffer of audio samples to be played in an audio stream
+ */
+class Buffer {
+public:
+    using Tag = u64;
+
+    Buffer(Tag tag, std::vector<u8>&& data) : tag{tag}, data{std::move(data)} {}
+
+    /// Returns the raw audio data for the buffer
+    const std::vector<u8>& GetData() const {
+        return data;
+    }
+
+    /// Returns the buffer tag, this is provided by the game to the audout service
+    Tag GetTag() const {
+        return tag;
+    }
+
+private:
+    Tag tag;
+    std::vector<u8> data;
+};
+
+} // namespace AudioCore
diff --git a/src/audio_core/stream.cpp b/src/audio_core/stream.cpp
new file mode 100644
index 0000000000..82bff4b9ef
--- /dev/null
+++ b/src/audio_core/stream.cpp
@@ -0,0 +1,103 @@
+// Copyright 2018 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "common/assert.h"
+#include "common/logging/log.h"
+#include "core/core_timing.h"
+#include "core/core_timing_util.h"
+
+#include "audio_core/stream.h"
+
+namespace AudioCore {
+
+constexpr size_t MaxAudioBufferCount{32};
+
+/// Returns the sample size for the specified audio stream format
+static size_t SampleSizeFromFormat(Stream::Format format) {
+    switch (format) {
+    case Stream::Format::Mono16:
+        return 2;
+    case Stream::Format::Stereo16:
+        return 4;
+    case Stream::Format::Multi51Channel16:
+        return 12;
+    };
+
+    LOG_CRITICAL(Audio, "Unimplemented format={}", static_cast<u32>(format));
+    UNREACHABLE();
+    return {};
+}
+
+Stream::Stream(int sample_rate, Format format, ReleaseCallback&& release_callback)
+    : sample_rate{sample_rate}, format{format}, release_callback{std::move(release_callback)} {
+    release_event = CoreTiming::RegisterEvent(
+        "Stream::Release", [this](u64 userdata, int cycles_late) { ReleaseActiveBuffer(); });
+}
+
+void Stream::Play() {
+    state = State::Playing;
+    PlayNextBuffer();
+}
+
+void Stream::Stop() {
+    ASSERT_MSG(false, "Unimplemented");
+}
+
+s64 Stream::GetBufferReleaseCycles(const Buffer& buffer) const {
+    const size_t num_samples{buffer.GetData().size() / SampleSizeFromFormat(format)};
+    return CoreTiming::usToCycles((static_cast<u64>(num_samples) * 1000000) / sample_rate);
+}
+
+void Stream::PlayNextBuffer() {
+    if (!IsPlaying()) {
+        // Ensure we are in playing state before playing the next buffer
+        return;
+    }
+
+    if (active_buffer) {
+        // Do not queue a new buffer if we are already playing a buffer
+        return;
+    }
+
+    if (queued_buffers.empty()) {
+        // No queued buffers - we are effectively paused
+        return;
+    }
+
+    active_buffer = queued_buffers.front();
+    queued_buffers.pop();
+
+    CoreTiming::ScheduleEventThreadsafe(GetBufferReleaseCycles(*active_buffer), release_event, {});
+}
+
+void Stream::ReleaseActiveBuffer() {
+    released_buffers.push(std::move(active_buffer));
+    release_callback();
+    PlayNextBuffer();
+}
+
+bool Stream::QueueBuffer(BufferPtr&& buffer) {
+    if (queued_buffers.size() < MaxAudioBufferCount) {
+        queued_buffers.push(std::move(buffer));
+        PlayNextBuffer();
+        return true;
+    }
+    return false;
+}
+
+bool Stream::ContainsBuffer(Buffer::Tag tag) const {
+    ASSERT_MSG(false, "Unimplemented");
+    return {};
+}
+
+std::vector<Buffer::Tag> Stream::GetTagsAndReleaseBuffers(size_t max_count) {
+    std::vector<Buffer::Tag> tags;
+    for (size_t count = 0; count < max_count && !released_buffers.empty(); ++count) {
+        tags.push_back(released_buffers.front()->GetTag());
+        released_buffers.pop();
+    }
+    return tags;
+}
+
+} // namespace AudioCore
diff --git a/src/audio_core/stream.h b/src/audio_core/stream.h
new file mode 100644
index 0000000000..5f43b0798f
--- /dev/null
+++ b/src/audio_core/stream.h
@@ -0,0 +1,89 @@
+// Copyright 2018 yuzu Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <functional>
+#include <memory>
+#include <vector>
+#include <queue>
+
+#include "audio_core/buffer.h"
+#include "common/assert.h"
+#include "common/common_types.h"
+#include "core/core_timing.h"
+
+namespace AudioCore {
+
+using BufferPtr = std::shared_ptr<Buffer>;
+
+/**
+ * Represents an audio stream, which is a sequence of queued buffers, to be outputed by AudioOut
+ */
+class Stream {
+public:
+    /// Audio format of the stream
+    enum class Format {
+        Mono16,
+        Stereo16,
+        Multi51Channel16,
+    };
+
+    /// Callback function type, used to change guest state on a buffer being released
+    using ReleaseCallback = std::function<void()>;
+
+    Stream(int sample_rate, Format format, ReleaseCallback&& release_callback);
+
+    /// Plays the audio stream
+    void Play();
+
+    /// Stops the audio stream
+    void Stop();
+
+    /// Queues a buffer into the audio stream, returns true on success
+    bool QueueBuffer(BufferPtr&& buffer);
+
+    /// Returns true if the audio stream contains a buffer with the specified tag
+    bool ContainsBuffer(Buffer::Tag tag) const;
+
+    /// Returns a vector of recently released buffers specified by tag
+    std::vector<Buffer::Tag> GetTagsAndReleaseBuffers(size_t max_count);
+
+    /// Returns true if the stream is currently playing
+    bool IsPlaying() const {
+        return state == State::Playing;
+    }
+
+    /// Returns the number of queued buffers
+    size_t GetQueueSize() const {
+        return queued_buffers.size();
+    }
+
+private:
+    /// Current state of the stream
+    enum class State {
+        Stopped,
+        Playing,
+    };
+
+    /// Plays the next queued buffer in the audio stream, starting playback if necessary
+    void PlayNextBuffer();
+
+    /// Releases the actively playing buffer, signalling that it has been completed
+    void ReleaseActiveBuffer();
+
+    /// Gets the number of core cycles when the specified buffer will be released
+    s64 GetBufferReleaseCycles(const Buffer& buffer) const;
+
+    int sample_rate;                        ///< Sample rate of the stream
+    Format format;                          ///< Format of the stream
+    ReleaseCallback release_callback;       ///< Buffer release callback for the stream
+    State state{State::Stopped};            ///< Playback state of the stream
+    CoreTiming::EventType* release_event{}; ///< Core timing release event for the stream
+    BufferPtr active_buffer;                ///< Actively playing buffer in the stream
+    std::queue<BufferPtr> queued_buffers;   ///< Buffers queued to be played in the stream
+    std::queue<BufferPtr> released_buffers; ///< Buffers recently released from the stream
+};
+
+} // namespace AudioCore
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index b367c2a279..95dbba6781 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -305,7 +305,7 @@ add_library(core STATIC
 
 create_target_directory_groups(core)
 
-target_link_libraries(core PUBLIC common PRIVATE video_core)
+target_link_libraries(core PUBLIC common PRIVATE audio_core video_core)
 target_link_libraries(core PUBLIC Boost::boost PRIVATE fmt lz4_static unicorn)
 
 if (ARCHITECTURE_x86_64)