summaryrefslogtreecommitdiffstats
path: root/src/audio_core
diff options
context:
space:
mode:
Diffstat (limited to 'src/audio_core')
-rw-r--r--src/audio_core/CMakeLists.txt23
-rw-r--r--src/audio_core/audio_core.cpp46
-rw-r--r--src/audio_core/audio_core.h7
-rw-r--r--src/audio_core/hle/common.h11
-rw-r--r--src/audio_core/hle/dsp.cpp69
-rw-r--r--src/audio_core/hle/dsp.h40
-rw-r--r--src/audio_core/hle/filter.h1
-rw-r--r--src/audio_core/hle/pipe.cpp41
-rw-r--r--src/audio_core/hle/pipe.h16
-rw-r--r--src/audio_core/hle/source.cpp320
-rw-r--r--src/audio_core/hle/source.h144
-rw-r--r--src/audio_core/interpolate.cpp85
-rw-r--r--src/audio_core/interpolate.h41
-rw-r--r--src/audio_core/null_sink.h29
-rw-r--r--src/audio_core/sdl2_sink.cpp126
-rw-r--r--src/audio_core/sdl2_sink.h30
-rw-r--r--src/audio_core/sink.h2
-rw-r--r--src/audio_core/sink_details.cpp25
-rw-r--r--src/audio_core/sink_details.h27
19 files changed, 1014 insertions, 69 deletions
diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt
index 869da5e83..13b5e400e 100644
--- a/src/audio_core/CMakeLists.txt
+++ b/src/audio_core/CMakeLists.txt
@@ -4,6 +4,9 @@ set(SRCS
hle/dsp.cpp
hle/filter.cpp
hle/pipe.cpp
+ hle/source.cpp
+ interpolate.cpp
+ sink_details.cpp
)
set(HEADERS
@@ -13,9 +16,27 @@ set(HEADERS
hle/dsp.h
hle/filter.h
hle/pipe.h
+ hle/source.h
+ interpolate.h
+ null_sink.h
sink.h
+ sink_details.h
)
+include_directories(../../externals/soundtouch/include)
+
+if(SDL2_FOUND)
+ set(SRCS ${SRCS} sdl2_sink.cpp)
+ set(HEADERS ${HEADERS} sdl2_sink.h)
+ include_directories(${SDL2_INCLUDE_DIR})
+endif()
+
create_directory_groups(${SRCS} ${HEADERS})
-add_library(audio_core STATIC ${SRCS} ${HEADERS}) \ No newline at end of file
+add_library(audio_core STATIC ${SRCS} ${HEADERS})
+target_link_libraries(audio_core SoundTouch)
+
+if(SDL2_FOUND)
+ target_link_libraries(audio_core ${SDL2_LIBRARY})
+ set_property(TARGET audio_core APPEND PROPERTY COMPILE_DEFINITIONS HAVE_SDL2)
+endif()
diff --git a/src/audio_core/audio_core.cpp b/src/audio_core/audio_core.cpp
index 894f46990..d42249ebd 100644
--- a/src/audio_core/audio_core.cpp
+++ b/src/audio_core/audio_core.cpp
@@ -2,8 +2,15 @@
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
+#include <memory>
+#include <string>
+
#include "audio_core/audio_core.h"
#include "audio_core/hle/dsp.h"
+#include "audio_core/hle/pipe.h"
+#include "audio_core/null_sink.h"
+#include "audio_core/sink.h"
+#include "audio_core/sink_details.h"
#include "core/core_timing.h"
#include "core/hle/kernel/vm_manager.h"
@@ -17,17 +24,16 @@ static constexpr u64 audio_frame_ticks = 1310252ull; ///< Units: ARM11 cycles
static void AudioTickCallback(u64 /*userdata*/, int cycles_late) {
if (DSP::HLE::Tick()) {
- // HACK: We're not signaling the interrups when they should be, but just firing them all off together.
- // It should be only (interrupt_id = 2, channel_id = 2) that's signalled here.
- // TODO(merry): Understand when the other interrupts are fired.
- DSP_DSP::SignalAllInterrupts();
+ // TODO(merry): Signal all the other interrupts as appropriate.
+ DSP_DSP::SignalPipeInterrupt(DSP::HLE::DspPipe::Audio);
+ // HACK(merry): Added to prevent regressions. Will remove soon.
+ DSP_DSP::SignalPipeInterrupt(DSP::HLE::DspPipe::Binary);
}
// Reschedule recurrent event
CoreTiming::ScheduleEvent(audio_frame_ticks - cycles_late, tick_event);
}
-/// Initialise Audio
void Init() {
DSP::HLE::Init();
@@ -35,19 +41,39 @@ void Init() {
CoreTiming::ScheduleEvent(audio_frame_ticks, tick_event);
}
-/// Add DSP address spaces to Process's address space.
void AddAddressSpace(Kernel::VMManager& address_space) {
- auto r0_vma = address_space.MapBackingMemory(DSP::HLE::region0_base, reinterpret_cast<u8*>(&DSP::HLE::g_region0), sizeof(DSP::HLE::SharedMemory), Kernel::MemoryState::IO).MoveFrom();
+ auto r0_vma = address_space.MapBackingMemory(DSP::HLE::region0_base, reinterpret_cast<u8*>(&DSP::HLE::g_regions[0]), sizeof(DSP::HLE::SharedMemory), Kernel::MemoryState::IO).MoveFrom();
address_space.Reprotect(r0_vma, Kernel::VMAPermission::ReadWrite);
- auto r1_vma = address_space.MapBackingMemory(DSP::HLE::region1_base, reinterpret_cast<u8*>(&DSP::HLE::g_region1), sizeof(DSP::HLE::SharedMemory), Kernel::MemoryState::IO).MoveFrom();
+ auto r1_vma = address_space.MapBackingMemory(DSP::HLE::region1_base, reinterpret_cast<u8*>(&DSP::HLE::g_regions[1]), sizeof(DSP::HLE::SharedMemory), Kernel::MemoryState::IO).MoveFrom();
address_space.Reprotect(r1_vma, Kernel::VMAPermission::ReadWrite);
}
-/// Shutdown Audio
+void SelectSink(std::string sink_id) {
+ if (sink_id == "auto") {
+ // Auto-select.
+ // g_sink_details is ordered in terms of desirability, with the best choice at the front.
+ const auto& sink_detail = g_sink_details.front();
+ DSP::HLE::SetSink(sink_detail.factory());
+ return;
+ }
+
+ auto iter = std::find_if(g_sink_details.begin(), g_sink_details.end(), [sink_id](const auto& sink_detail) {
+ return sink_detail.id == sink_id;
+ });
+
+ if (iter == g_sink_details.end()) {
+ LOG_ERROR(Audio, "AudioCore::SelectSink given invalid sink_id");
+ DSP::HLE::SetSink(std::make_unique<NullSink>());
+ return;
+ }
+
+ DSP::HLE::SetSink(iter->factory());
+}
+
void Shutdown() {
CoreTiming::UnscheduleEvent(tick_event, 0);
DSP::HLE::Shutdown();
}
-} //namespace
+} // namespace AudioCore
diff --git a/src/audio_core/audio_core.h b/src/audio_core/audio_core.h
index 64c330914..f618361f3 100644
--- a/src/audio_core/audio_core.h
+++ b/src/audio_core/audio_core.h
@@ -4,14 +4,14 @@
#pragma once
+#include <string>
+
namespace Kernel {
class VMManager;
}
namespace AudioCore {
-constexpr int num_sources = 24;
-constexpr int samples_per_frame = 160; ///< Samples per audio frame at native sample rate
constexpr int native_sample_rate = 32728; ///< 32kHz
/// Initialise Audio Core
@@ -20,6 +20,9 @@ void Init();
/// Add DSP address spaces to a Process.
void AddAddressSpace(Kernel::VMManager& vm_manager);
+/// Select the sink to use based on sink id.
+void SelectSink(std::string sink_id);
+
/// Shutdown Audio Core
void Shutdown();
diff --git a/src/audio_core/hle/common.h b/src/audio_core/hle/common.h
index 37d441eb2..596b67eaf 100644
--- a/src/audio_core/hle/common.h
+++ b/src/audio_core/hle/common.h
@@ -7,18 +7,19 @@
#include <algorithm>
#include <array>
-#include "audio_core/audio_core.h"
-
#include "common/common_types.h"
namespace DSP {
namespace HLE {
+constexpr int num_sources = 24;
+constexpr int samples_per_frame = 160; ///< Samples per audio frame at native sample rate
+
/// The final output to the speakers is stereo. Preprocessing output in Source is also stereo.
-using StereoFrame16 = std::array<std::array<s16, 2>, AudioCore::samples_per_frame>;
+using StereoFrame16 = std::array<std::array<s16, 2>, samples_per_frame>;
/// The DSP is quadraphonic internally.
-using QuadFrame32 = std::array<std::array<s32, 4>, AudioCore::samples_per_frame>;
+using QuadFrame32 = std::array<std::array<s32, 4>, samples_per_frame>;
/**
* This performs the filter operation defined by FilterT::ProcessSample on the frame in-place.
@@ -26,7 +27,7 @@ using QuadFrame32 = std::array<std::array<s32, 4>, AudioCore::samples_per_fram
*/
template<typename FrameT, typename FilterT>
void FilterFrame(FrameT& frame, FilterT& filter) {
- std::transform(frame.begin(), frame.end(), frame.begin(), [&filter](const typename FrameT::value_type& sample) {
+ std::transform(frame.begin(), frame.end(), frame.begin(), [&filter](const auto& sample) {
return filter.ProcessSample(sample);
});
}
diff --git a/src/audio_core/hle/dsp.cpp b/src/audio_core/hle/dsp.cpp
index c89356edc..0cdbdb06a 100644
--- a/src/audio_core/hle/dsp.cpp
+++ b/src/audio_core/hle/dsp.cpp
@@ -2,40 +2,81 @@
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
+#include <array>
+#include <memory>
+
#include "audio_core/hle/dsp.h"
#include "audio_core/hle/pipe.h"
+#include "audio_core/hle/source.h"
+#include "audio_core/sink.h"
namespace DSP {
namespace HLE {
-SharedMemory g_region0;
-SharedMemory g_region1;
+std::array<SharedMemory, 2> g_regions;
+
+static size_t CurrentRegionIndex() {
+ // The region with the higher frame counter is chosen unless there is wraparound.
+ // This function only returns a 0 or 1.
+
+ if (g_regions[0].frame_counter == 0xFFFFu && g_regions[1].frame_counter != 0xFFFEu) {
+ // Wraparound has occured.
+ return 1;
+ }
+
+ if (g_regions[1].frame_counter == 0xFFFFu && g_regions[0].frame_counter != 0xFFFEu) {
+ // Wraparound has occured.
+ return 0;
+ }
+
+ return (g_regions[0].frame_counter > g_regions[1].frame_counter) ? 0 : 1;
+}
+
+static SharedMemory& ReadRegion() {
+ return g_regions[CurrentRegionIndex()];
+}
+
+static SharedMemory& WriteRegion() {
+ return g_regions[1 - CurrentRegionIndex()];
+}
+
+static std::array<Source, num_sources> sources = {
+ Source(0), Source(1), Source(2), Source(3), Source(4), Source(5),
+ Source(6), Source(7), Source(8), Source(9), Source(10), Source(11),
+ Source(12), Source(13), Source(14), Source(15), Source(16), Source(17),
+ Source(18), Source(19), Source(20), Source(21), Source(22), Source(23)
+};
+
+static std::unique_ptr<AudioCore::Sink> sink;
void Init() {
DSP::HLE::ResetPipes();
+ for (auto& source : sources) {
+ source.Reset();
+ }
}
void Shutdown() {
}
bool Tick() {
- return true;
-}
+ SharedMemory& read = ReadRegion();
+ SharedMemory& write = WriteRegion();
-SharedMemory& CurrentRegion() {
- // The region with the higher frame counter is chosen unless there is wraparound.
+ std::array<QuadFrame32, 3> intermediate_mixes = {};
- if (g_region0.frame_counter == 0xFFFFu && g_region1.frame_counter != 0xFFFEu) {
- // Wraparound has occured.
- return g_region1;
+ for (size_t i = 0; i < num_sources; i++) {
+ write.source_statuses.status[i] = sources[i].Tick(read.source_configurations.config[i], read.adpcm_coefficients.coeff[i]);
+ for (size_t mix = 0; mix < 3; mix++) {
+ sources[i].MixInto(intermediate_mixes[mix], mix);
+ }
}
- if (g_region1.frame_counter == 0xFFFFu && g_region0.frame_counter != 0xFFFEu) {
- // Wraparound has occured.
- return g_region0;
- }
+ return true;
+}
- return (g_region0.frame_counter > g_region1.frame_counter) ? g_region0 : g_region1;
+void SetSink(std::unique_ptr<AudioCore::Sink> sink_) {
+ sink = std::move(sink_);
}
} // namespace HLE
diff --git a/src/audio_core/hle/dsp.h b/src/audio_core/hle/dsp.h
index c15ef0b7a..f6e53f68f 100644
--- a/src/audio_core/hle/dsp.h
+++ b/src/audio_core/hle/dsp.h
@@ -4,16 +4,22 @@
#pragma once
+#include <array>
#include <cstddef>
+#include <memory>
#include <type_traits>
-#include "audio_core/audio_core.h"
+#include "audio_core/hle/common.h"
#include "common/bit_field.h"
#include "common/common_funcs.h"
#include "common/common_types.h"
#include "common/swap.h"
+namespace AudioCore {
+class Sink;
+}
+
namespace DSP {
namespace HLE {
@@ -27,13 +33,8 @@ namespace HLE {
// double-buffer. The frame counter is located as the very last u16 of each region and is incremented
// each audio tick.
-struct SharedMemory;
-
constexpr VAddr region0_base = 0x1FF50000;
-extern SharedMemory g_region0;
-
constexpr VAddr region1_base = 0x1FF70000;
-extern SharedMemory g_region1;
/**
* The DSP is native 16-bit. The DSP also appears to be big-endian. When reading 32-bit numbers from
@@ -164,9 +165,9 @@ struct SourceConfiguration {
float_le rate_multiplier;
enum class InterpolationMode : u8 {
- None = 0,
+ Polyphase = 0,
Linear = 1,
- Polyphase = 2
+ None = 2
};
InterpolationMode interpolation_mode;
@@ -305,7 +306,7 @@ struct SourceConfiguration {
u16_le buffer_id;
};
- Configuration config[AudioCore::num_sources];
+ Configuration config[num_sources];
};
ASSERT_DSP_STRUCT(SourceConfiguration::Configuration, 192);
ASSERT_DSP_STRUCT(SourceConfiguration::Configuration::Buffer, 20);
@@ -313,14 +314,14 @@ ASSERT_DSP_STRUCT(SourceConfiguration::Configuration::Buffer, 20);
struct SourceStatus {
struct Status {
u8 is_enabled; ///< Is this channel enabled? (Doesn't have to be playing anything.)
- u8 previous_buffer_id_dirty; ///< Non-zero when previous_buffer_id changes
+ u8 current_buffer_id_dirty; ///< Non-zero when current_buffer_id changes
u16_le sync; ///< Is set by the DSP to the value of SourceConfiguration::sync
u32_dsp buffer_position; ///< Number of samples into the current buffer
- u16_le previous_buffer_id; ///< Updated when a buffer finishes playing
+ u16_le current_buffer_id; ///< Updated when a buffer finishes playing
INSERT_PADDING_DSPWORDS(1);
};
- Status status[AudioCore::num_sources];
+ Status status[num_sources];
};
ASSERT_DSP_STRUCT(SourceStatus::Status, 12);
@@ -413,7 +414,7 @@ ASSERT_DSP_STRUCT(DspConfiguration::ReverbEffect, 52);
struct AdpcmCoefficients {
/// Coefficients are signed fixed point with 11 fractional bits.
/// Each source has 16 coefficients associated with it.
- s16_le coeff[AudioCore::num_sources][16];
+ s16_le coeff[num_sources][16];
};
ASSERT_DSP_STRUCT(AdpcmCoefficients, 768);
@@ -427,7 +428,7 @@ ASSERT_DSP_STRUCT(DspStatus, 32);
/// Final mixed output in PCM16 stereo format, what you hear out of the speakers.
/// When the application writes to this region it has no effect.
struct FinalMixSamples {
- s16_le pcm16[2 * AudioCore::samples_per_frame];
+ s16_le pcm16[2 * samples_per_frame];
};
ASSERT_DSP_STRUCT(FinalMixSamples, 640);
@@ -437,7 +438,7 @@ ASSERT_DSP_STRUCT(FinalMixSamples, 640);
/// Values that exceed s16 range will be clipped by the DSP after further processing.
struct IntermediateMixSamples {
struct Samples {
- s32_le pcm32[4][AudioCore::samples_per_frame]; ///< Little-endian as opposed to DSP middle-endian.
+ s32_le pcm32[4][samples_per_frame]; ///< Little-endian as opposed to DSP middle-endian.
};
Samples mix1;
@@ -502,6 +503,8 @@ struct SharedMemory {
};
ASSERT_DSP_STRUCT(SharedMemory, 0x8000);
+extern std::array<SharedMemory, 2> g_regions;
+
// Structures must have an offset that is a multiple of two.
static_assert(offsetof(SharedMemory, frame_counter) % 2 == 0, "Structures in DSP::HLE::SharedMemory must be 2-byte aligned");
static_assert(offsetof(SharedMemory, source_configurations) % 2 == 0, "Structures in DSP::HLE::SharedMemory must be 2-byte aligned");
@@ -535,8 +538,11 @@ void Shutdown();
*/
bool Tick();
-/// Returns a mutable reference to the current region. Current region is selected based on the frame counter.
-SharedMemory& CurrentRegion();
+/**
+ * Set the output sink. This must be called before calling Tick().
+ * @param sink The sink to which audio will be output to.
+ */
+void SetSink(std::unique_ptr<AudioCore::Sink> sink);
} // namespace HLE
} // namespace DSP
diff --git a/src/audio_core/hle/filter.h b/src/audio_core/hle/filter.h
index 75738f600..43d2035cd 100644
--- a/src/audio_core/hle/filter.h
+++ b/src/audio_core/hle/filter.h
@@ -16,6 +16,7 @@ namespace HLE {
/// Preprocessing filters. There is an independent set of filters for each Source.
class SourceFilters final {
+public:
SourceFilters() { Reset(); }
/// Reset internal state.
diff --git a/src/audio_core/hle/pipe.cpp b/src/audio_core/hle/pipe.cpp
index 9381883b4..44dff1345 100644
--- a/src/audio_core/hle/pipe.cpp
+++ b/src/audio_core/hle/pipe.cpp
@@ -12,12 +12,14 @@
#include "common/common_types.h"
#include "common/logging/log.h"
+#include "core/hle/service/dsp_dsp.h"
+
namespace DSP {
namespace HLE {
static DspState dsp_state = DspState::Off;
-static std::array<std::vector<u8>, static_cast<size_t>(DspPipe::DspPipe_MAX)> pipe_data;
+static std::array<std::vector<u8>, NUM_DSP_PIPE> pipe_data;
void ResetPipes() {
for (auto& data : pipe_data) {
@@ -27,17 +29,24 @@ void ResetPipes() {
}
std::vector<u8> PipeRead(DspPipe pipe_number, u32 length) {
- if (pipe_number >= DspPipe::DspPipe_MAX) {
- LOG_ERROR(Audio_DSP, "pipe_number = %u invalid", pipe_number);
+ const size_t pipe_index = static_cast<size_t>(pipe_number);
+
+ if (pipe_index >= NUM_DSP_PIPE) {
+ LOG_ERROR(Audio_DSP, "pipe_number = %zu invalid", pipe_index);
return {};
}
- std::vector<u8>& data = pipe_data[static_cast<size_t>(pipe_number)];
+ if (length > UINT16_MAX) { // Can only read at most UINT16_MAX from the pipe
+ LOG_ERROR(Audio_DSP, "length of %u greater than max of %u", length, UINT16_MAX);
+ return {};
+ }
+
+ std::vector<u8>& data = pipe_data[pipe_index];
if (length > data.size()) {
- LOG_WARNING(Audio_DSP, "pipe_number = %u is out of data, application requested read of %u but %zu remain",
- pipe_number, length, data.size());
- length = data.size();
+ LOG_WARNING(Audio_DSP, "pipe_number = %zu is out of data, application requested read of %u but %zu remain",
+ pipe_index, length, data.size());
+ length = static_cast<u32>(data.size());
}
if (length == 0)
@@ -49,16 +58,20 @@ std::vector<u8> PipeRead(DspPipe pipe_number, u32 length) {
}
size_t GetPipeReadableSize(DspPipe pipe_number) {
- if (pipe_number >= DspPipe::DspPipe_MAX) {
- LOG_ERROR(Audio_DSP, "pipe_number = %u invalid", pipe_number);
+ const size_t pipe_index = static_cast<size_t>(pipe_number);
+
+ if (pipe_index >= NUM_DSP_PIPE) {
+ LOG_ERROR(Audio_DSP, "pipe_number = %zu invalid", pipe_index);
return 0;
}
- return pipe_data[static_cast<size_t>(pipe_number)].size();
+ return pipe_data[pipe_index].size();
}
static void WriteU16(DspPipe pipe_number, u16 value) {
- std::vector<u8>& data = pipe_data[static_cast<size_t>(pipe_number)];
+ const size_t pipe_index = static_cast<size_t>(pipe_number);
+
+ std::vector<u8>& data = pipe_data.at(pipe_index);
// Little endian
data.emplace_back(value & 0xFF);
data.emplace_back(value >> 8);
@@ -86,11 +99,13 @@ static void AudioPipeWriteStructAddresses() {
};
// Begin with a u16 denoting the number of structs.
- WriteU16(DspPipe::Audio, struct_addresses.size());
+ WriteU16(DspPipe::Audio, static_cast<u16>(struct_addresses.size()));
// Then write the struct addresses.
for (u16 addr : struct_addresses) {
WriteU16(DspPipe::Audio, addr);
}
+ // Signal that we have data on this pipe.
+ DSP_DSP::SignalPipeInterrupt(DspPipe::Audio);
}
void PipeWrite(DspPipe pipe_number, const std::vector<u8>& buffer) {
@@ -145,7 +160,7 @@ void PipeWrite(DspPipe pipe_number, const std::vector<u8>& buffer) {
return;
}
default:
- LOG_CRITICAL(Audio_DSP, "pipe_number = %u unimplemented", pipe_number);
+ LOG_CRITICAL(Audio_DSP, "pipe_number = %zu unimplemented", static_cast<size_t>(pipe_number));
UNIMPLEMENTED();
return;
}
diff --git a/src/audio_core/hle/pipe.h b/src/audio_core/hle/pipe.h
index 382d35e87..b714c0496 100644
--- a/src/audio_core/hle/pipe.h
+++ b/src/audio_core/hle/pipe.h
@@ -19,15 +19,19 @@ enum class DspPipe {
Debug = 0,
Dma = 1,
Audio = 2,
- Binary = 3,
- DspPipe_MAX
+ Binary = 3
};
+constexpr size_t NUM_DSP_PIPE = 8;
/**
- * Read a DSP pipe.
- * @param pipe_number The Pipe ID
- * @param length How much data to request.
- * @return The data read from the pipe. The size of this vector can be less than the length requested.
+ * Reads `length` bytes from the DSP pipe identified with `pipe_number`.
+ * @note Can read up to the maximum value of a u16 in bytes (65,535).
+ * @note IF an error is encoutered with either an invalid `pipe_number` or `length` value, an empty vector will be returned.
+ * @note IF `length` is set to 0, an empty vector will be returned.
+ * @note IF `length` is greater than the amount of data available, this function will only read the available amount.
+ * @param pipe_number a `DspPipe`
+ * @param length the number of bytes to read. The max is 65,535 (max of u16).
+ * @returns a vector of bytes from the specified pipe. On error, will be empty.
*/
std::vector<u8> PipeRead(DspPipe pipe_number, u32 length);
diff --git a/src/audio_core/hle/source.cpp b/src/audio_core/hle/source.cpp
new file mode 100644
index 000000000..daaf6e3f3
--- /dev/null
+++ b/src/audio_core/hle/source.cpp
@@ -0,0 +1,320 @@
+// Copyright 2016 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <algorithm>
+#include <array>
+
+#include "audio_core/codec.h"
+#include "audio_core/hle/common.h"
+#include "audio_core/hle/source.h"
+#include "audio_core/interpolate.h"
+
+#include "common/assert.h"
+#include "common/logging/log.h"
+
+#include "core/memory.h"
+
+namespace DSP {
+namespace HLE {
+
+SourceStatus::Status Source::Tick(SourceConfiguration::Configuration& config, const s16_le (&adpcm_coeffs)[16]) {
+ ParseConfig(config, adpcm_coeffs);
+
+ if (state.enabled) {
+ GenerateFrame();
+ }
+
+ return GetCurrentStatus();
+}
+
+void Source::MixInto(QuadFrame32& dest, size_t intermediate_mix_id) const {
+ if (!state.enabled)
+ return;
+
+ const std::array<float, 4>& gains = state.gain.at(intermediate_mix_id);
+ for (size_t samplei = 0; samplei < samples_per_frame; samplei++) {
+ // Conversion from stereo (current_frame) to quadraphonic (dest) occurs here.
+ dest[samplei][0] += static_cast<s32>(gains[0] * current_frame[samplei][0]);
+ dest[samplei][1] += static_cast<s32>(gains[1] * current_frame[samplei][1]);
+ dest[samplei][2] += static_cast<s32>(gains[2] * current_frame[samplei][0]);
+ dest[samplei][3] += static_cast<s32>(gains[3] * current_frame[samplei][1]);
+ }
+}
+
+void Source::Reset() {
+ current_frame.fill({});
+ state = {};
+}
+
+void Source::ParseConfig(SourceConfiguration::Configuration& config, const s16_le (&adpcm_coeffs)[16]) {
+ if (!config.dirty_raw) {
+ return;
+ }
+
+ if (config.reset_flag) {
+ config.reset_flag.Assign(0);
+ Reset();
+ LOG_TRACE(Audio_DSP, "source_id=%zu reset", source_id);
+ }
+
+ if (config.partial_reset_flag) {
+ config.partial_reset_flag.Assign(0);
+ state.input_queue = std::priority_queue<Buffer, std::vector<Buffer>, BufferOrder>{};
+ LOG_TRACE(Audio_DSP, "source_id=%zu partial_reset", source_id);
+ }
+
+ if (config.enable_dirty) {
+ config.enable_dirty.Assign(0);
+ state.enabled = config.enable != 0;
+ LOG_TRACE(Audio_DSP, "source_id=%zu enable=%d", source_id, state.enabled);
+ }
+
+ if (config.sync_dirty) {
+ config.sync_dirty.Assign(0);
+ state.sync = config.sync;
+ LOG_TRACE(Audio_DSP, "source_id=%zu sync=%u", source_id, state.sync);
+ }
+
+ if (config.rate_multiplier_dirty) {
+ config.rate_multiplier_dirty.Assign(0);
+ state.rate_multiplier = config.rate_multiplier;
+ LOG_TRACE(Audio_DSP, "source_id=%zu rate=%f", source_id, state.rate_multiplier);
+
+ if (state.rate_multiplier <= 0) {
+ LOG_ERROR(Audio_DSP, "Was given an invalid rate multiplier: source_id=%zu rate=%f", source_id, state.rate_multiplier);
+ state.rate_multiplier = 1.0f;
+ // Note: Actual firmware starts producing garbage if this occurs.
+ }
+ }
+
+ if (config.adpcm_coefficients_dirty) {
+ config.adpcm_coefficients_dirty.Assign(0);
+ std::transform(adpcm_coeffs, adpcm_coeffs + state.adpcm_coeffs.size(), state.adpcm_coeffs.begin(),
+ [](const auto& coeff) { return static_cast<s16>(coeff); });
+ LOG_TRACE(Audio_DSP, "source_id=%zu adpcm update", source_id);
+ }
+
+ if (config.gain_0_dirty) {
+ config.gain_0_dirty.Assign(0);
+ std::transform(config.gain[0], config.gain[0] + state.gain[0].size(), state.gain[0].begin(),
+ [](const auto& coeff) { return static_cast<float>(coeff); });
+ LOG_TRACE(Audio_DSP, "source_id=%zu gain 0 update", source_id);
+ }
+
+ if (config.gain_1_dirty) {
+ config.gain_1_dirty.Assign(0);
+ std::transform(config.gain[1], config.gain[1] + state.gain[1].size(), state.gain[1].begin(),
+ [](const auto& coeff) { return static_cast<float>(coeff); });
+ LOG_TRACE(Audio_DSP, "source_id=%zu gain 1 update", source_id);
+ }
+
+ if (config.gain_2_dirty) {
+ config.gain_2_dirty.Assign(0);
+ std::transform(config.gain[2], config.gain[2] + state.gain[2].size(), state.gain[2].begin(),
+ [](const auto& coeff) { return static_cast<float>(coeff); });
+ LOG_TRACE(Audio_DSP, "source_id=%zu gain 2 update", source_id);
+ }
+
+ if (config.filters_enabled_dirty) {
+ config.filters_enabled_dirty.Assign(0);
+ state.filters.Enable(config.simple_filter_enabled.ToBool(), config.biquad_filter_enabled.ToBool());
+ LOG_TRACE(Audio_DSP, "source_id=%zu enable_simple=%hu enable_biquad=%hu",
+ source_id, config.simple_filter_enabled.Value(), config.biquad_filter_enabled.Value());
+ }
+
+ if (config.simple_filter_dirty) {
+ config.simple_filter_dirty.Assign(0);
+ state.filters.Configure(config.simple_filter);
+ LOG_TRACE(Audio_DSP, "source_id=%zu simple filter update");
+ }
+
+ if (config.biquad_filter_dirty) {
+ config.biquad_filter_dirty.Assign(0);
+ state.filters.Configure(config.biquad_filter);
+ LOG_TRACE(Audio_DSP, "source_id=%zu biquad filter update");
+ }
+
+ if (config.interpolation_dirty) {
+ config.interpolation_dirty.Assign(0);
+ state.interpolation_mode = config.interpolation_mode;
+ LOG_TRACE(Audio_DSP, "source_id=%zu interpolation_mode=%zu", source_id, static_cast<size_t>(state.interpolation_mode));
+ }
+
+ if (config.format_dirty || config.embedded_buffer_dirty) {
+ config.format_dirty.Assign(0);
+ state.format = config.format;
+ LOG_TRACE(Audio_DSP, "source_id=%zu format=%zu", source_id, static_cast<size_t>(state.format));
+ }
+
+ if (config.mono_or_stereo_dirty || config.embedded_buffer_dirty) {
+ config.mono_or_stereo_dirty.Assign(0);
+ state.mono_or_stereo = config.mono_or_stereo;
+ LOG_TRACE(Audio_DSP, "source_id=%zu mono_or_stereo=%zu", source_id, static_cast<size_t>(state.mono_or_stereo));
+ }
+
+ if (config.embedded_buffer_dirty) {
+ config.embedded_buffer_dirty.Assign(0);
+ state.input_queue.emplace(Buffer{
+ config.physical_address,
+ config.length,
+ static_cast<u8>(config.adpcm_ps),
+ { config.adpcm_yn[0], config.adpcm_yn[1] },
+ config.adpcm_dirty.ToBool(),
+ config.is_looping.ToBool(),
+ config.buffer_id,
+ state.mono_or_stereo,
+ state.format,
+ false
+ });
+ LOG_TRACE(Audio_DSP, "enqueuing embedded addr=0x%08x len=%u id=%hu", config.physical_address, config.length, config.buffer_id);
+ }
+
+ if (config.buffer_queue_dirty) {
+ config.buffer_queue_dirty.Assign(0);
+ for (size_t i = 0; i < 4; i++) {
+ if (config.buffers_dirty & (1 << i)) {
+ const auto& b = config.buffers[i];
+ state.input_queue.emplace(Buffer{
+ b.physical_address,
+ b.length,
+ static_cast<u8>(b.adpcm_ps),
+ { b.adpcm_yn[0], b.adpcm_yn[1] },
+ b.adpcm_dirty != 0,
+ b.is_looping != 0,
+ b.buffer_id,
+ state.mono_or_stereo,
+ state.format,
+ true
+ });
+ LOG_TRACE(Audio_DSP, "enqueuing queued %zu addr=0x%08x len=%u id=%hu", i, b.physical_address, b.length, b.buffer_id);
+ }
+ }
+ config.buffers_dirty = 0;
+ }
+
+ if (config.dirty_raw) {
+ LOG_DEBUG(Audio_DSP, "source_id=%zu remaining_dirty=%x", source_id, config.dirty_raw);
+ }
+
+ config.dirty_raw = 0;
+}
+
+void Source::GenerateFrame() {
+ current_frame.fill({});
+
+ if (state.current_buffer.empty() && !DequeueBuffer()) {
+ state.enabled = false;
+ state.buffer_update = true;
+ state.current_buffer_id = 0;
+ return;
+ }
+
+ size_t frame_position = 0;
+
+ state.current_sample_number = state.next_sample_number;
+ while (frame_position < current_frame.size()) {
+ if (state.current_buffer.empty() && !DequeueBuffer()) {
+ break;
+ }
+
+ const size_t size_to_copy = std::min(state.current_buffer.size(), current_frame.size() - frame_position);
+
+ std::copy(state.current_buffer.begin(), state.current_buffer.begin() + size_to_copy, current_frame.begin() + frame_position);
+ state.current_buffer.erase(state.current_buffer.begin(), state.current_buffer.begin() + size_to_copy);
+
+ frame_position += size_to_copy;
+ state.next_sample_number += static_cast<u32>(size_to_copy);
+ }
+
+ state.filters.ProcessFrame(current_frame);
+}
+
+
+bool Source::DequeueBuffer() {
+ ASSERT_MSG(state.current_buffer.empty(), "Shouldn't dequeue; we still have data in current_buffer");
+
+ if (state.input_queue.empty())
+ return false;
+
+ const Buffer buf = state.input_queue.top();
+ state.input_queue.pop();
+
+ if (buf.adpcm_dirty) {
+ state.adpcm_state.yn1 = buf.adpcm_yn[0];
+ state.adpcm_state.yn2 = buf.adpcm_yn[1];
+ }
+
+ if (buf.is_looping) {
+ LOG_ERROR(Audio_DSP, "Looped buffers are unimplemented at the moment");
+ }
+
+ const u8* const memory = Memory::GetPhysicalPointer(buf.physical_address);
+ if (memory) {
+ const unsigned num_channels = buf.mono_or_stereo == MonoOrStereo::Stereo ? 2 : 1;
+ switch (buf.format) {
+ case Format::PCM8:
+ state.current_buffer = Codec::DecodePCM8(num_channels, memory, buf.length);
+ break;
+ case Format::PCM16:
+ state.current_buffer = Codec::DecodePCM16(num_channels, memory, buf.length);
+ break;
+ case Format::ADPCM:
+ DEBUG_ASSERT(num_channels == 1);
+ state.current_buffer = Codec::DecodeADPCM(memory, buf.length, state.adpcm_coeffs, state.adpcm_state);
+ break;
+ default:
+ UNIMPLEMENTED();
+ break;
+ }
+ } else {
+ LOG_WARNING(Audio_DSP, "source_id=%zu buffer_id=%hu length=%u: Invalid physical address 0x%08X",
+ source_id, buf.buffer_id, buf.length, buf.physical_address);
+ state.current_buffer.clear();
+ return true;
+ }
+
+ switch (state.interpolation_mode) {
+ case InterpolationMode::None:
+ state.current_buffer = AudioInterp::None(state.interp_state, state.current_buffer, state.rate_multiplier);
+ break;
+ case InterpolationMode::Linear:
+ state.current_buffer = AudioInterp::Linear(state.interp_state, state.current_buffer, state.rate_multiplier);
+ break;
+ case InterpolationMode::Polyphase:
+ // TODO(merry): Implement polyphase interpolation
+ state.current_buffer = AudioInterp::Linear(state.interp_state, state.current_buffer, state.rate_multiplier);
+ break;
+ default:
+ UNIMPLEMENTED();
+ break;
+ }
+
+ state.current_sample_number = 0;
+ state.next_sample_number = 0;
+ state.current_buffer_id = buf.buffer_id;
+ state.buffer_update = buf.from_queue;
+
+ LOG_TRACE(Audio_DSP, "source_id=%zu buffer_id=%hu from_queue=%s current_buffer.size()=%zu",
+ source_id, buf.buffer_id, buf.from_queue ? "true" : "false", state.current_buffer.size());
+ return true;
+}
+
+SourceStatus::Status Source::GetCurrentStatus() {
+ SourceStatus::Status ret;
+
+ // Applications depend on the correct emulation of
+ // current_buffer_id_dirty and current_buffer_id to synchronise
+ // audio with video.
+ ret.is_enabled = state.enabled;
+ ret.current_buffer_id_dirty = state.buffer_update ? 1 : 0;
+ state.buffer_update = false;
+ ret.current_buffer_id = state.current_buffer_id;
+ ret.buffer_position = state.current_sample_number;
+ ret.sync = state.sync;
+
+ return ret;
+}
+
+} // namespace HLE
+} // namespace DSP
diff --git a/src/audio_core/hle/source.h b/src/audio_core/hle/source.h
new file mode 100644
index 000000000..7ee08d424
--- /dev/null
+++ b/src/audio_core/hle/source.h
@@ -0,0 +1,144 @@
+// Copyright 2016 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <array>
+#include <queue>
+#include <vector>
+
+#include "audio_core/codec.h"
+#include "audio_core/hle/common.h"
+#include "audio_core/hle/dsp.h"
+#include "audio_core/hle/filter.h"
+#include "audio_core/interpolate.h"
+
+#include "common/common_types.h"
+
+namespace DSP {
+namespace HLE {
+
+/**
+ * This module performs:
+ * - Buffer management
+ * - Decoding of buffers
+ * - Buffer resampling and interpolation
+ * - Per-source filtering (SimpleFilter, BiquadFilter)
+ * - Per-source gain
+ * - Other per-source processing
+ */
+class Source final {
+public:
+ explicit Source(size_t source_id_) : source_id(source_id_) {
+ Reset();
+ }
+
+ /// Resets internal state.
+ void Reset();
+
+ /**
+ * This is called once every audio frame. This performs per-source processing every frame.
+ * @param config The new configuration we've got for this Source from the application.
+ * @param adpcm_coeffs ADPCM coefficients to use if config tells us to use them (may contain invalid values otherwise).
+ * @return The current status of this Source. This is given back to the emulated application via SharedMemory.
+ */
+ SourceStatus::Status Tick(SourceConfiguration::Configuration& config, const s16_le (&adpcm_coeffs)[16]);
+
+ /**
+ * Mix this source's output into dest, using the gains for the `intermediate_mix_id`-th intermediate mixer.
+ * @param dest The QuadFrame32 to mix into.
+ * @param intermediate_mix_id The id of the intermediate mix whose gains we are using.
+ */
+ void MixInto(QuadFrame32& dest, size_t intermediate_mix_id) const;
+
+private:
+ const size_t source_id;
+ StereoFrame16 current_frame;
+
+ using Format = SourceConfiguration::Configuration::Format;
+ using InterpolationMode = SourceConfiguration::Configuration::InterpolationMode;
+ using MonoOrStereo = SourceConfiguration::Configuration::MonoOrStereo;
+
+ /// Internal representation of a buffer for our buffer queue
+ struct Buffer {
+ PAddr physical_address;
+ u32 length;
+ u8 adpcm_ps;
+ std::array<u16, 2> adpcm_yn;
+ bool adpcm_dirty;
+ bool is_looping;
+ u16 buffer_id;
+
+ MonoOrStereo mono_or_stereo;
+ Format format;
+
+ bool from_queue;
+ };
+
+ struct BufferOrder {
+ bool operator() (const Buffer& a, const Buffer& b) const {
+ // Lower buffer_id comes first.
+ return a.buffer_id > b.buffer_id;
+ }
+ };
+
+ struct {
+
+ // State variables
+
+ bool enabled = false;
+ u16 sync = 0;
+
+ // Mixing
+
+ std::array<std::array<float, 4>, 3> gain = {};
+
+ // Buffer queue
+
+ std::priority_queue<Buffer, std::vector<Buffer>, BufferOrder> input_queue;
+ MonoOrStereo mono_or_stereo = MonoOrStereo::Mono;
+ Format format = Format::ADPCM;
+
+ // Current buffer
+
+ u32 current_sample_number = 0;
+ u32 next_sample_number = 0;
+ std::vector<std::array<s16, 2>> current_buffer;
+
+ // buffer_id state
+
+ bool buffer_update = false;
+ u32 current_buffer_id = 0;
+
+ // Decoding state
+
+ std::array<s16, 16> adpcm_coeffs = {};
+ Codec::ADPCMState adpcm_state = {};
+
+ // Resampling state
+
+ float rate_multiplier = 1.0;
+ InterpolationMode interpolation_mode = InterpolationMode::Polyphase;
+ AudioInterp::State interp_state = {};
+
+ // Filter state
+
+ SourceFilters filters;
+
+ } state;
+
+ // Internal functions
+
+ /// INTERNAL: Update our internal state based on the current config.
+ void ParseConfig(SourceConfiguration::Configuration& config, const s16_le (&adpcm_coeffs)[16]);
+ /// INTERNAL: Generate the current audio output for this frame based on our internal state.
+ void GenerateFrame();
+ /// INTERNAL: Dequeues a buffer and does preprocessing on it (decoding, resampling). Puts it into current_buffer.
+ bool DequeueBuffer();
+ /// INTERNAL: Generates a SourceStatus::Status based on our internal state.
+ SourceStatus::Status GetCurrentStatus();
+};
+
+} // namespace HLE
+} // namespace DSP
diff --git a/src/audio_core/interpolate.cpp b/src/audio_core/interpolate.cpp
new file mode 100644
index 000000000..fcd3aa066
--- /dev/null
+++ b/src/audio_core/interpolate.cpp
@@ -0,0 +1,85 @@
+// Copyright 2016 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "audio_core/interpolate.h"
+
+#include "common/assert.h"
+#include "common/math_util.h"
+
+namespace AudioInterp {
+
+// Calculations are done in fixed point with 24 fractional bits.
+// (This is not verified. This was chosen for minimal error.)
+constexpr u64 scale_factor = 1 << 24;
+constexpr u64 scale_mask = scale_factor - 1;
+
+/// Here we step over the input in steps of rate_multiplier, until we consume all of the input.
+/// Three adjacent samples are passed to fn each step.
+template <typename Function>
+static StereoBuffer16 StepOverSamples(State& state, const StereoBuffer16& input, float rate_multiplier, Function fn) {
+ ASSERT(rate_multiplier > 0);
+
+ if (input.size() < 2)
+ return {};
+
+ StereoBuffer16 output;
+ output.reserve(static_cast<size_t>(input.size() / rate_multiplier));
+
+ u64 step_size = static_cast<u64>(rate_multiplier * scale_factor);
+
+ u64 fposition = 0;
+ const u64 max_fposition = input.size() * scale_factor;
+
+ while (fposition < 1 * scale_factor) {
+ u64 fraction = fposition & scale_mask;
+
+ output.push_back(fn(fraction, state.xn2, state.xn1, input[0]));
+
+ fposition += step_size;
+ }
+
+ while (fposition < 2 * scale_factor) {
+ u64 fraction = fposition & scale_mask;
+
+ output.push_back(fn(fraction, state.xn1, input[0], input[1]));
+
+ fposition += step_size;
+ }
+
+ while (fposition < max_fposition) {
+ u64 fraction = fposition & scale_mask;
+
+ size_t index = static_cast<size_t>(fposition / scale_factor);
+ output.push_back(fn(fraction, input[index - 2], input[index - 1], input[index]));
+
+ fposition += step_size;
+ }
+
+ state.xn2 = input[input.size() - 2];
+ state.xn1 = input[input.size() - 1];
+
+ return output;
+}
+
+StereoBuffer16 None(State& state, const StereoBuffer16& input, float rate_multiplier) {
+ return StepOverSamples(state, input, rate_multiplier, [](u64 fraction, const auto& x0, const auto& x1, const auto& x2) {
+ return x0;
+ });
+}
+
+StereoBuffer16 Linear(State& state, const StereoBuffer16& input, float rate_multiplier) {
+ // Note on accuracy: Some values that this produces are +/- 1 from the actual firmware.
+ return StepOverSamples(state, input, rate_multiplier, [](u64 fraction, const auto& x0, const auto& x1, const auto& x2) {
+ // This is a saturated subtraction. (Verified by black-box fuzzing.)
+ s64 delta0 = MathUtil::Clamp<s64>(x1[0] - x0[0], -32768, 32767);
+ s64 delta1 = MathUtil::Clamp<s64>(x1[1] - x0[1], -32768, 32767);
+
+ return std::array<s16, 2> {
+ static_cast<s16>(x0[0] + fraction * delta0 / scale_factor),
+ static_cast<s16>(x0[1] + fraction * delta1 / scale_factor)
+ };
+ });
+}
+
+} // namespace AudioInterp
diff --git a/src/audio_core/interpolate.h b/src/audio_core/interpolate.h
new file mode 100644
index 000000000..a4c0a453d
--- /dev/null
+++ b/src/audio_core/interpolate.h
@@ -0,0 +1,41 @@
+// Copyright 2016 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <array>
+#include <vector>
+
+#include "common/common_types.h"
+
+namespace AudioInterp {
+
+/// A variable length buffer of signed PCM16 stereo samples.
+using StereoBuffer16 = std::vector<std::array<s16, 2>>;
+
+struct State {
+ // Two historical samples.
+ std::array<s16, 2> xn1 = {}; ///< x[n-1]
+ std::array<s16, 2> xn2 = {}; ///< x[n-2]
+};
+
+/**
+ * No interpolation. This is equivalent to a zero-order hold. There is a two-sample predelay.
+ * @param input Input buffer.
+ * @param rate_multiplier Stretch factor. Must be a positive non-zero value.
+ * rate_multiplier > 1.0 performs decimation and rate_multipler < 1.0 performs upsampling.
+ * @return The resampled audio buffer.
+ */
+StereoBuffer16 None(State& state, const StereoBuffer16& input, float rate_multiplier);
+
+/**
+ * Linear interpolation. This is equivalent to a first-order hold. There is a two-sample predelay.
+ * @param input Input buffer.
+ * @param rate_multiplier Stretch factor. Must be a positive non-zero value.
+ * rate_multiplier > 1.0 performs decimation and rate_multipler < 1.0 performs upsampling.
+ * @return The resampled audio buffer.
+ */
+StereoBuffer16 Linear(State& state, const StereoBuffer16& input, float rate_multiplier);
+
+} // namespace AudioInterp
diff --git a/src/audio_core/null_sink.h b/src/audio_core/null_sink.h
new file mode 100644
index 000000000..faf0ee4e1
--- /dev/null
+++ b/src/audio_core/null_sink.h
@@ -0,0 +1,29 @@
+// Copyright 2016 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <cstddef>
+
+#include "audio_core/audio_core.h"
+#include "audio_core/sink.h"
+
+namespace AudioCore {
+
+class NullSink final : public Sink {
+public:
+ ~NullSink() override = default;
+
+ unsigned int GetNativeSampleRate() const override {
+ return native_sample_rate;
+ }
+
+ void EnqueueSamples(const std::vector<s16>&) override {}
+
+ size_t SamplesInQueue() const override {
+ return 0;
+ }
+};
+
+} // namespace AudioCore
diff --git a/src/audio_core/sdl2_sink.cpp b/src/audio_core/sdl2_sink.cpp
new file mode 100644
index 000000000..dc75c04ee
--- /dev/null
+++ b/src/audio_core/sdl2_sink.cpp
@@ -0,0 +1,126 @@
+// Copyright 2016 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <list>
+#include <vector>
+
+#include <SDL.h>
+
+#include "audio_core/audio_core.h"
+#include "audio_core/sdl2_sink.h"
+
+#include "common/assert.h"
+#include "common/logging/log.h"
+#include <numeric>
+
+namespace AudioCore {
+
+struct SDL2Sink::Impl {
+ unsigned int sample_rate = 0;
+
+ SDL_AudioDeviceID audio_device_id = 0;
+
+ std::list<std::vector<s16>> queue;
+
+ static void Callback(void* impl_, u8* buffer, int buffer_size_in_bytes);
+};
+
+SDL2Sink::SDL2Sink() : impl(std::make_unique<Impl>()) {
+ if (SDL_Init(SDL_INIT_AUDIO) < 0) {
+ LOG_CRITICAL(Audio_Sink, "SDL_Init(SDL_INIT_AUDIO) failed");
+ impl->audio_device_id = 0;
+ return;
+ }
+
+ SDL_AudioSpec desired_audiospec;
+ SDL_zero(desired_audiospec);
+ desired_audiospec.format = AUDIO_S16;
+ desired_audiospec.channels = 2;
+ desired_audiospec.freq = native_sample_rate;
+ desired_audiospec.samples = 1024;
+ desired_audiospec.userdata = impl.get();
+ desired_audiospec.callback = &Impl::Callback;
+
+ SDL_AudioSpec obtained_audiospec;
+ SDL_zero(obtained_audiospec);
+
+ impl->audio_device_id = SDL_OpenAudioDevice(nullptr, false, &desired_audiospec, &obtained_audiospec, 0);
+ if (impl->audio_device_id <= 0) {
+ LOG_CRITICAL(Audio_Sink, "SDL_OpenAudioDevice failed");
+ return;
+ }
+
+ impl->sample_rate = obtained_audiospec.freq;
+
+ // SDL2 audio devices start out paused, unpause it:
+ SDL_PauseAudioDevice(impl->audio_device_id, 0);
+}
+
+SDL2Sink::~SDL2Sink() {
+ if (impl->audio_device_id <= 0)
+ return;
+
+ SDL_CloseAudioDevice(impl->audio_device_id);
+}
+
+unsigned int SDL2Sink::GetNativeSampleRate() const {
+ if (impl->audio_device_id <= 0)
+ return native_sample_rate;
+
+ return impl->sample_rate;
+}
+
+void SDL2Sink::EnqueueSamples(const std::vector<s16>& samples) {
+ if (impl->audio_device_id <= 0)
+ return;
+
+ ASSERT_MSG(samples.size() % 2 == 0, "Samples must be in interleaved stereo PCM16 format (size must be a multiple of two)");
+
+ SDL_LockAudioDevice(impl->audio_device_id);
+ impl->queue.emplace_back(samples);
+ SDL_UnlockAudioDevice(impl->audio_device_id);
+}
+
+size_t SDL2Sink::SamplesInQueue() const {
+ if (impl->audio_device_id <= 0)
+ return 0;
+
+ SDL_LockAudioDevice(impl->audio_device_id);
+
+ size_t total_size = std::accumulate(impl->queue.begin(), impl->queue.end(), static_cast<size_t>(0),
+ [](size_t sum, const auto& buffer) {
+ // Division by two because each stereo sample is made of two s16.
+ return sum + buffer.size() / 2;
+ });
+
+ SDL_UnlockAudioDevice(impl->audio_device_id);
+
+ return total_size;
+}
+
+void SDL2Sink::Impl::Callback(void* impl_, u8* buffer, int buffer_size_in_bytes) {
+ Impl* impl = reinterpret_cast<Impl*>(impl_);
+
+ size_t remaining_size = static_cast<size_t>(buffer_size_in_bytes) / sizeof(s16); // Keep track of size in 16-bit increments.
+
+ while (remaining_size > 0 && !impl->queue.empty()) {
+ if (impl->queue.front().size() <= remaining_size) {
+ memcpy(buffer, impl->queue.front().data(), impl->queue.front().size() * sizeof(s16));
+ buffer += impl->queue.front().size() * sizeof(s16);
+ remaining_size -= impl->queue.front().size();
+ impl->queue.pop_front();
+ } else {
+ memcpy(buffer, impl->queue.front().data(), remaining_size * sizeof(s16));
+ buffer += remaining_size * sizeof(s16);
+ impl->queue.front().erase(impl->queue.front().begin(), impl->queue.front().begin() + remaining_size);
+ remaining_size = 0;
+ }
+ }
+
+ if (remaining_size > 0) {
+ memset(buffer, 0, remaining_size * sizeof(s16));
+ }
+}
+
+} // namespace AudioCore
diff --git a/src/audio_core/sdl2_sink.h b/src/audio_core/sdl2_sink.h
new file mode 100644
index 000000000..0f296b673
--- /dev/null
+++ b/src/audio_core/sdl2_sink.h
@@ -0,0 +1,30 @@
+// Copyright 2016 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <cstddef>
+#include <memory>
+
+#include "audio_core/sink.h"
+
+namespace AudioCore {
+
+class SDL2Sink final : public Sink {
+public:
+ SDL2Sink();
+ ~SDL2Sink() override;
+
+ unsigned int GetNativeSampleRate() const override;
+
+ void EnqueueSamples(const std::vector<s16>& samples) override;
+
+ size_t SamplesInQueue() const override;
+
+private:
+ struct Impl;
+ std::unique_ptr<Impl> impl;
+};
+
+} // namespace AudioCore
diff --git a/src/audio_core/sink.h b/src/audio_core/sink.h
index cad21a85e..1c881c3d2 100644
--- a/src/audio_core/sink.h
+++ b/src/audio_core/sink.h
@@ -19,7 +19,7 @@ public:
virtual ~Sink() = default;
/// The native rate of this sink. The sink expects to be fed samples that respect this. (Units: samples/sec)
- virtual unsigned GetNativeSampleRate() const = 0;
+ virtual unsigned int GetNativeSampleRate() const = 0;
/**
* Feed stereo samples to sink.
diff --git a/src/audio_core/sink_details.cpp b/src/audio_core/sink_details.cpp
new file mode 100644
index 000000000..ba5e83d17
--- /dev/null
+++ b/src/audio_core/sink_details.cpp
@@ -0,0 +1,25 @@
+// Copyright 2016 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <memory>
+#include <vector>
+
+#include "audio_core/null_sink.h"
+#include "audio_core/sink_details.h"
+
+#ifdef HAVE_SDL2
+#include "audio_core/sdl2_sink.h"
+#endif
+
+namespace AudioCore {
+
+// g_sink_details is ordered in terms of desirability, with the best choice at the top.
+const std::vector<SinkDetails> g_sink_details = {
+#ifdef HAVE_SDL2
+ { "sdl2", []() { return std::make_unique<SDL2Sink>(); } },
+#endif
+ { "null", []() { return std::make_unique<NullSink>(); } },
+};
+
+} // namespace AudioCore
diff --git a/src/audio_core/sink_details.h b/src/audio_core/sink_details.h
new file mode 100644
index 000000000..4b30cf835
--- /dev/null
+++ b/src/audio_core/sink_details.h
@@ -0,0 +1,27 @@
+// Copyright 2016 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <functional>
+#include <memory>
+#include <vector>
+
+namespace AudioCore {
+
+class Sink;
+
+struct SinkDetails {
+ SinkDetails(const char* id_, std::function<std::unique_ptr<Sink>()> factory_)
+ : id(id_), factory(factory_) {}
+
+ /// Name for this sink.
+ const char* id;
+ /// A method to call to construct an instance of this type of sink.
+ std::function<std::unique_ptr<Sink>()> factory;
+};
+
+extern const std::vector<SinkDetails> g_sink_details;
+
+} // namespace AudioCore