From 5f59815f39f7f9817ea0b55885e89fdf02189f78 Mon Sep 17 00:00:00 2001 From: bunnei Date: Fri, 27 Jul 2018 19:17:27 -0400 Subject: [PATCH 1/5] externals: Add cubeb for audio output. --- .gitmodules | 3 +++ externals/cubeb | 1 + 2 files changed, 4 insertions(+) create mode 160000 externals/cubeb diff --git a/.gitmodules b/.gitmodules index 4de47fa6b7..0f94613c4f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,6 +7,9 @@ [submodule "catch"] path = externals/catch url = https://github.com/philsquared/Catch.git +[submodule "cubeb"] + path = externals/cubeb + url = https://github.com/kinetiknz/cubeb.git [submodule "dynarmic"] path = externals/dynarmic url = https://github.com/MerryMage/dynarmic.git diff --git a/externals/cubeb b/externals/cubeb new file mode 160000 index 0000000000..12b78c0edf --- /dev/null +++ b/externals/cubeb @@ -0,0 +1 @@ +Subproject commit 12b78c0edfa40007e41dbdcd9dfe367fbb98d018 From eaf66b4c9f9517bcb4f417260ad460fca4254bec Mon Sep 17 00:00:00 2001 From: bunnei Date: Sat, 28 Jul 2018 13:27:59 -0400 Subject: [PATCH 2/5] audio_core: Move to audout_u impl. - This is necessary so streams are created on the same thread. --- src/core/core.cpp | 2 -- src/core/core.h | 7 ------- src/core/hle/service/audio/audout_u.cpp | 8 ++++---- src/core/hle/service/audio/audout_u.h | 2 ++ 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/core/core.cpp b/src/core/core.cpp index 186fa46dfd..b7f4b45323 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -177,7 +177,6 @@ System::ResultStatus System::Init(EmuWindow* emu_window, u32 system_mode) { } gpu_core = std::make_unique(); - audio_core = std::make_unique(); telemetry_session = std::make_unique(); service_manager = std::make_shared(); @@ -229,7 +228,6 @@ void System::Shutdown() { service_manager.reset(); telemetry_session.reset(); gpu_core.reset(); - audio_core.reset(); // Close all CPU/threading state cpu_barrier->NotifyEnd(); diff --git a/src/core/core.h b/src/core/core.h index 6f4df775f3..c123fe4018 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -8,7 +8,6 @@ #include #include #include -#include "audio_core/audio_out.h" #include "common/common_types.h" #include "core/arm/exclusive_monitor.h" #include "core/core_cpu.h" @@ -132,11 +131,6 @@ public: return *gpu_core; } - /// Gets the AudioCore interface - AudioCore::AudioOut& AudioCore() { - return *audio_core; - } - /// Gets the scheduler for the CPU core that is currently running Kernel::Scheduler& CurrentScheduler() { return *CurrentCpuCore().Scheduler(); @@ -201,7 +195,6 @@ private: /// AppLoader used to load the current executing application std::unique_ptr app_loader; std::unique_ptr gpu_core; - std::unique_ptr audio_core; std::shared_ptr debug_context; Kernel::SharedPtr current_process; std::shared_ptr cpu_exclusive_monitor; diff --git a/src/core/hle/service/audio/audout_u.cpp b/src/core/hle/service/audio/audout_u.cpp index a15d53ff82..ab37c2a694 100644 --- a/src/core/hle/service/audio/audout_u.cpp +++ b/src/core/hle/service/audio/audout_u.cpp @@ -25,9 +25,8 @@ constexpr int DefaultSampleRate{48000}; class IAudioOut final : public ServiceFramework { public: - IAudioOut(AudoutParams audio_params) - : ServiceFramework("IAudioOut"), audio_params(audio_params), - audio_core(Core::System::GetInstance().AudioCore()) { + IAudioOut(AudoutParams audio_params, AudioCore::AudioOut& audio_core) + : ServiceFramework("IAudioOut"), audio_params(audio_params), audio_core(audio_core) { static const FunctionInfo functions[] = { {0, &IAudioOut::GetAudioOutState, "GetAudioOutState"}, @@ -195,7 +194,7 @@ void AudOutU::OpenAudioOutImpl(Kernel::HLERequestContext& ctx) { // TODO(bunnei): Support more than one IAudioOut interface. When we add this, ListAudioOutsImpl // will likely need to be updated as well. ASSERT_MSG(!audio_out_interface, "Unimplemented"); - audio_out_interface = std::make_shared(std::move(params)); + audio_out_interface = std::make_shared(std::move(params), *audio_core); IPC::ResponseBuilder rb{ctx, 6, 0, 1}; rb.Push(RESULT_SUCCESS); @@ -212,6 +211,7 @@ AudOutU::AudOutU() : ServiceFramework("audout:u") { {2, &AudOutU::ListAudioOutsImpl, "ListAudioOutsAuto"}, {3, &AudOutU::OpenAudioOutImpl, "OpenAudioOutAuto"}}; RegisterHandlers(functions); + audio_core = std::make_unique(); } } // namespace Service::Audio diff --git a/src/core/hle/service/audio/audout_u.h b/src/core/hle/service/audio/audout_u.h index bc43f1f447..e5c2184d58 100644 --- a/src/core/hle/service/audio/audout_u.h +++ b/src/core/hle/service/audio/audout_u.h @@ -4,6 +4,7 @@ #pragma once +#include "audio_core/audio_out.h" #include "core/hle/service/service.h" namespace Kernel { @@ -33,6 +34,7 @@ public: private: std::shared_ptr audio_out_interface; + std::unique_ptr audio_core; void ListAudioOutsImpl(Kernel::HLERequestContext& ctx); void OpenAudioOutImpl(Kernel::HLERequestContext& ctx); From 0e8a2c7222b978507f62d7e0b83187b16532eae8 Mon Sep 17 00:00:00 2001 From: bunnei Date: Sat, 28 Jul 2018 13:35:22 -0400 Subject: [PATCH 3/5] audio_core: Misc. improvements to stream/buffer/audio_out. --- src/audio_core/audio_out.cpp | 4 ++-- src/audio_core/audio_out.h | 6 ++---- src/audio_core/buffer.h | 3 +++ src/audio_core/stream.cpp | 24 ++++++++++++------------ src/audio_core/stream.h | 15 +++++++++++++-- 5 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/audio_core/audio_out.cpp b/src/audio_core/audio_out.cpp index 6d418a05b1..43414f197d 100644 --- a/src/audio_core/audio_out.cpp +++ b/src/audio_core/audio_out.cpp @@ -9,7 +9,7 @@ namespace AudioCore { /// Returns the stream format from the specified number of channels -static Stream::Format ChannelsToStreamFormat(int num_channels) { +static Stream::Format ChannelsToStreamFormat(u32 num_channels) { switch (num_channels) { case 1: return Stream::Format::Mono16; @@ -24,7 +24,7 @@ static Stream::Format ChannelsToStreamFormat(int num_channels) { return {}; } -StreamPtr AudioOut::OpenStream(int sample_rate, int num_channels, +StreamPtr AudioOut::OpenStream(u32 sample_rate, u32 num_channels, Stream::ReleaseCallback&& release_callback) { streams.push_back(std::make_shared(sample_rate, ChannelsToStreamFormat(num_channels), std::move(release_callback))); diff --git a/src/audio_core/audio_out.h b/src/audio_core/audio_out.h index a86499d103..962360d09f 100644 --- a/src/audio_core/audio_out.h +++ b/src/audio_core/audio_out.h @@ -13,15 +13,13 @@ namespace AudioCore { -using StreamPtr = std::shared_ptr; - /** * 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, + StreamPtr OpenStream(u32 sample_rate, u32 num_channels, Stream::ReleaseCallback&& release_callback); /// Returns a vector of recently released buffers specified by tag for the specified stream @@ -37,7 +35,7 @@ public: bool QueueBuffer(StreamPtr stream, Buffer::Tag tag, std::vector&& data); private: - /// Active audio streams on the interface + SinkPtr sink; std::vector streams; }; diff --git a/src/audio_core/buffer.h b/src/audio_core/buffer.h index 874ec787e2..4bf5fd58a7 100644 --- a/src/audio_core/buffer.h +++ b/src/audio_core/buffer.h @@ -4,6 +4,7 @@ #pragma once +#include #include #include "common/common_types.h" @@ -34,4 +35,6 @@ private: std::vector data; }; +using BufferPtr = std::shared_ptr; + } // namespace AudioCore diff --git a/src/audio_core/stream.cpp b/src/audio_core/stream.cpp index 82bff4b9ef..63edc6c8da 100644 --- a/src/audio_core/stream.cpp +++ b/src/audio_core/stream.cpp @@ -13,24 +13,24 @@ namespace AudioCore { constexpr size_t MaxAudioBufferCount{32}; -/// Returns the sample size for the specified audio stream format -static size_t SampleSizeFromFormat(Stream::Format format) { +u32 Stream::GetNumChannels() const { switch (format) { - case Stream::Format::Mono16: + case Format::Mono16: + return 1; + case Format::Stereo16: return 2; - case Stream::Format::Stereo16: - return 4; - case Stream::Format::Multi51Channel16: - return 12; - }; - + case Format::Multi51Channel16: + return 6; + } LOG_CRITICAL(Audio, "Unimplemented format={}", static_cast(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)} { +u32 Stream::GetSampleSize() const { + return GetNumChannels() * 2; +} + release_event = CoreTiming::RegisterEvent( "Stream::Release", [this](u64 userdata, int cycles_late) { ReleaseActiveBuffer(); }); } @@ -45,7 +45,7 @@ void Stream::Stop() { } s64 Stream::GetBufferReleaseCycles(const Buffer& buffer) const { - const size_t num_samples{buffer.GetData().size() / SampleSizeFromFormat(format)}; + const size_t num_samples{buffer.GetData().size() / GetSampleSize()}; return CoreTiming::usToCycles((static_cast(num_samples) * 1000000) / sample_rate); } diff --git a/src/audio_core/stream.h b/src/audio_core/stream.h index 5f43b0798f..5c10058998 100644 --- a/src/audio_core/stream.h +++ b/src/audio_core/stream.h @@ -16,8 +16,6 @@ namespace AudioCore { -using BufferPtr = std::shared_ptr; - /** * Represents an audio stream, which is a sequence of queued buffers, to be outputed by AudioOut */ @@ -60,6 +58,17 @@ public: return queued_buffers.size(); } + /// Gets the sample rate + u32 GetSampleRate() const { + return sample_rate; + } + + /// Gets the number of channels + u32 GetNumChannels() const; + + /// Gets the sample size in bytes + u32 GetSampleSize() const; + private: /// Current state of the stream enum class State { @@ -86,4 +95,6 @@ private: std::queue released_buffers; ///< Buffers recently released from the stream }; +using StreamPtr = std::shared_ptr; + } // namespace AudioCore From 9ef227e09d6e374008d9536935d01c11a6ca06bd Mon Sep 17 00:00:00 2001 From: bunnei Date: Sat, 28 Jul 2018 13:40:50 -0400 Subject: [PATCH 4/5] audio_core: Add interfaces for Sink and SinkStream. --- src/audio_core/CMakeLists.txt | 5 +++++ src/audio_core/null_sink.h | 27 +++++++++++++++++++++++ src/audio_core/sink.h | 29 +++++++++++++++++++++++++ src/audio_core/sink_details.cpp | 38 +++++++++++++++++++++++++++++++++ src/audio_core/sink_details.h | 32 +++++++++++++++++++++++++++ src/audio_core/sink_stream.h | 32 +++++++++++++++++++++++++++ 6 files changed, 163 insertions(+) create mode 100644 src/audio_core/null_sink.h create mode 100644 src/audio_core/sink.h create mode 100644 src/audio_core/sink_details.cpp create mode 100644 src/audio_core/sink_details.h create mode 100644 src/audio_core/sink_stream.h diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt index f00a55994f..6bc8f586ae 100644 --- a/src/audio_core/CMakeLists.txt +++ b/src/audio_core/CMakeLists.txt @@ -2,8 +2,13 @@ add_library(audio_core STATIC audio_out.cpp audio_out.h buffer.h + null_sink.h stream.cpp stream.h + sink.h + sink_details.cpp + sink_details.h + sink_stream.h ) create_target_directory_groups(audio_core) diff --git a/src/audio_core/null_sink.h b/src/audio_core/null_sink.h new file mode 100644 index 0000000000..2e04438f78 --- /dev/null +++ b/src/audio_core/null_sink.h @@ -0,0 +1,27 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "audio_core/sink.h" + +namespace AudioCore { + +class NullSink final : public Sink { +public: + explicit NullSink(std::string){}; + ~NullSink() override = default; + + SinkStream& AcquireSinkStream(u32 /*sample_rate*/, u32 /*num_channels*/) override { + return null_sink_stream; + } + +private: + struct NullSinkStreamImpl final : SinkStream { + void EnqueueSamples(u32 /*num_channels*/, const s16* /*samples*/, + size_t /*sample_count*/) override {} + } null_sink_stream; +}; + +} // namespace AudioCore diff --git a/src/audio_core/sink.h b/src/audio_core/sink.h new file mode 100644 index 0000000000..d1bb98c3d9 --- /dev/null +++ b/src/audio_core/sink.h @@ -0,0 +1,29 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include + +#include "audio_core/sink_stream.h" +#include "common/common_types.h" + +namespace AudioCore { + +constexpr char auto_device_name[] = "auto"; + +/** + * This class is an interface for an audio sink. An audio sink accepts samples in stereo signed + * PCM16 format to be output. Sinks *do not* handle resampling and expect the correct sample rate. + * They are dumb outputs. + */ +class Sink { +public: + virtual ~Sink() = default; + virtual SinkStream& AcquireSinkStream(u32 sample_rate, u32 num_channels) = 0; +}; + +using SinkPtr = std::unique_ptr; + +} // namespace AudioCore diff --git a/src/audio_core/sink_details.cpp b/src/audio_core/sink_details.cpp new file mode 100644 index 0000000000..6396d80654 --- /dev/null +++ b/src/audio_core/sink_details.cpp @@ -0,0 +1,38 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#include "audio_core/null_sink.h" +#include "audio_core/sink_details.h" +#include "common/logging/log.h" + +namespace AudioCore { + +// g_sink_details is ordered in terms of desirability, with the best choice at the top. +const std::vector g_sink_details = { + SinkDetails{"null", &std::make_unique, + [] { return std::vector{"null"}; }}, +}; + +const SinkDetails& GetSinkDetails(std::string sink_id) { + 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 (sink_id == "auto" || iter == g_sink_details.end()) { + if (sink_id != "auto") { + LOG_ERROR(Audio, "AudioCore::SelectSink given invalid sink_id {}", sink_id); + } + // Auto-select. + // g_sink_details is ordered in terms of desirability, with the best choice at the front. + iter = g_sink_details.begin(); + } + + return *iter; +} + +} // namespace AudioCore diff --git a/src/audio_core/sink_details.h b/src/audio_core/sink_details.h new file mode 100644 index 0000000000..aa8aae1a99 --- /dev/null +++ b/src/audio_core/sink_details.h @@ -0,0 +1,32 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include + +namespace AudioCore { + +class Sink; + +struct SinkDetails { + SinkDetails(const char* id_, std::function(std::string)> factory_, + std::function()> list_devices_) + : id(id_), factory(factory_), list_devices(list_devices_) {} + + /// Name for this sink. + const char* id; + /// A method to call to construct an instance of this type of sink. + std::function(std::string device_id)> factory; + /// A method to call to list available devices. + std::function()> list_devices; +}; + +extern const std::vector g_sink_details; + +const SinkDetails& GetSinkDetails(std::string sink_id); + +} // namespace AudioCore diff --git a/src/audio_core/sink_stream.h b/src/audio_core/sink_stream.h new file mode 100644 index 0000000000..e7a3f01b0c --- /dev/null +++ b/src/audio_core/sink_stream.h @@ -0,0 +1,32 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include + +#include "common/common_types.h" + +namespace AudioCore { + +/** + * Accepts samples in stereo signed PCM16 format to be output. Sinks *do not* handle resampling and + * expect the correct sample rate. They are dumb outputs. + */ +class SinkStream { +public: + virtual ~SinkStream() = default; + + /** + * Feed stereo samples to sink. + * @param num_channels Number of channels used. + * @param samples Samples in interleaved stereo PCM16 format. + * @param sample_count Number of samples. + */ + virtual void EnqueueSamples(u32 num_channels, const s16* samples, size_t sample_count) = 0; +}; + +using SinkStreamPtr = std::unique_ptr; + +} // namespace AudioCore From f437c11caf2c1afc0b7d0fdb808be10d7b1adfcf Mon Sep 17 00:00:00 2001 From: bunnei Date: Sat, 28 Jul 2018 13:44:50 -0400 Subject: [PATCH 5/5] audio_core: Implement Sink and SinkStream interfaces with cubeb. --- CMakeLists.txt | 2 + externals/CMakeLists.txt | 6 + src/audio_core/CMakeLists.txt | 7 ++ src/audio_core/audio_out.cpp | 13 ++- src/audio_core/audio_out.h | 2 +- src/audio_core/cubeb_sink.cpp | 190 ++++++++++++++++++++++++++++++++ src/audio_core/cubeb_sink.h | 31 ++++++ src/audio_core/sink_details.cpp | 6 + src/audio_core/stream.cpp | 11 ++ src/audio_core/stream.h | 7 +- 10 files changed, 269 insertions(+), 6 deletions(-) create mode 100644 src/audio_core/cubeb_sink.cpp create mode 100644 src/audio_core/cubeb_sink.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 86d2423ed3..3639b623c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,8 @@ CMAKE_DEPENDENT_OPTION(YUZU_USE_BUNDLED_QT "Download bundled Qt binaries" ON "EN option(YUZU_USE_BUNDLED_UNICORN "Build/Download bundled Unicorn" ON) +option(ENABLE_CUBEB "Enables the cubeb audio backend" ON) + if(NOT EXISTS ${CMAKE_SOURCE_DIR}/.git/hooks/pre-commit) message(STATUS "Copying pre-commit hook") file(COPY hooks/pre-commit diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index e51671cb82..4636761e51 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -54,3 +54,9 @@ endif() # Opus add_subdirectory(opus) target_include_directories(opus INTERFACE ./opus/include) + +# Cubeb +if(ENABLE_CUBEB) + set(BUILD_TESTS OFF CACHE BOOL "") + add_subdirectory(cubeb) +endif() diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt index 6bc8f586ae..81121167d6 100644 --- a/src/audio_core/CMakeLists.txt +++ b/src/audio_core/CMakeLists.txt @@ -2,6 +2,8 @@ add_library(audio_core STATIC audio_out.cpp audio_out.h buffer.h + cubeb_sink.cpp + cubeb_sink.h null_sink.h stream.cpp stream.h @@ -14,3 +16,8 @@ add_library(audio_core STATIC create_target_directory_groups(audio_core) target_link_libraries(audio_core PUBLIC common core) + +if(ENABLE_CUBEB) + target_link_libraries(audio_core PRIVATE cubeb) + target_compile_definitions(audio_core PRIVATE -DHAVE_CUBEB=1) +endif() diff --git a/src/audio_core/audio_out.cpp b/src/audio_core/audio_out.cpp index 43414f197d..77cedb6ba3 100644 --- a/src/audio_core/audio_out.cpp +++ b/src/audio_core/audio_out.cpp @@ -3,6 +3,8 @@ // Refer to the license.txt file included. #include "audio_core/audio_out.h" +#include "audio_core/sink.h" +#include "audio_core/sink_details.h" #include "common/assert.h" #include "common/logging/log.h" @@ -26,9 +28,14 @@ static Stream::Format ChannelsToStreamFormat(u32 num_channels) { StreamPtr AudioOut::OpenStream(u32 sample_rate, u32 num_channels, Stream::ReleaseCallback&& release_callback) { - streams.push_back(std::make_shared(sample_rate, ChannelsToStreamFormat(num_channels), - std::move(release_callback))); - return streams.back(); + if (!sink) { + const SinkDetails& sink_details = GetSinkDetails("auto"); + sink = sink_details.factory(""); + } + + return std::make_shared(sample_rate, ChannelsToStreamFormat(num_channels), + std::move(release_callback), + sink->AcquireSinkStream(sample_rate, num_channels)); } std::vector AudioOut::GetTagsAndReleaseBuffers(StreamPtr stream, size_t max_count) { diff --git a/src/audio_core/audio_out.h b/src/audio_core/audio_out.h index 962360d09f..8d9b695d45 100644 --- a/src/audio_core/audio_out.h +++ b/src/audio_core/audio_out.h @@ -8,6 +8,7 @@ #include #include "audio_core/buffer.h" +#include "audio_core/sink.h" #include "audio_core/stream.h" #include "common/common_types.h" @@ -36,7 +37,6 @@ public: private: SinkPtr sink; - std::vector streams; }; } // namespace AudioCore diff --git a/src/audio_core/cubeb_sink.cpp b/src/audio_core/cubeb_sink.cpp new file mode 100644 index 0000000000..34ae5b0623 --- /dev/null +++ b/src/audio_core/cubeb_sink.cpp @@ -0,0 +1,190 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include + +#include "audio_core/cubeb_sink.h" +#include "audio_core/stream.h" +#include "common/logging/log.h" + +namespace AudioCore { + +class SinkStreamImpl final : public SinkStream { +public: + SinkStreamImpl(cubeb* ctx, cubeb_devid output_device) : ctx{ctx} { + cubeb_stream_params params; + params.rate = 48000; + params.channels = GetNumChannels(); + params.format = CUBEB_SAMPLE_S16NE; + params.layout = CUBEB_LAYOUT_STEREO; + + u32 minimum_latency = 0; + if (cubeb_get_min_latency(ctx, ¶ms, &minimum_latency) != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "Error getting minimum latency"); + } + + if (cubeb_stream_init(ctx, &stream_backend, "yuzu Audio Output", nullptr, nullptr, + output_device, ¶ms, std::max(512u, minimum_latency), + &SinkStreamImpl::DataCallback, &SinkStreamImpl::StateCallback, + this) != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "Error initializing cubeb stream"); + return; + } + + if (cubeb_stream_start(stream_backend) != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "Error starting cubeb stream"); + return; + } + } + + ~SinkStreamImpl() { + if (!ctx) { + return; + } + + if (cubeb_stream_stop(stream_backend) != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "Error stopping cubeb stream"); + } + + cubeb_stream_destroy(stream_backend); + } + + void EnqueueSamples(u32 num_channels, const s16* samples, size_t sample_count) override { + if (!ctx) { + return; + } + + queue.reserve(queue.size() + sample_count * GetNumChannels()); + + if (num_channels == 2) { + // Copy as-is + std::copy(samples, samples + sample_count * GetNumChannels(), + std::back_inserter(queue)); + } else if (num_channels == 6) { + // Downsample 6 channels to 2 + const size_t sample_count_copy_size = sample_count * num_channels * 2; + queue.reserve(sample_count_copy_size); + for (size_t i = 0; i < sample_count * num_channels; i += num_channels) { + queue.push_back(samples[i]); + queue.push_back(samples[i + 1]); + } + } else { + ASSERT_MSG(false, "Unimplemented"); + } + } + + u32 GetNumChannels() const { + // Only support 2-channel stereo output for now + return 2; + } + +private: + std::vector device_list; + + cubeb* ctx{}; + cubeb_stream* stream_backend{}; + + std::vector queue; + + static long DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer, + void* output_buffer, long num_frames); + static void StateCallback(cubeb_stream* stream, void* user_data, cubeb_state state); +}; + +CubebSink::CubebSink(std::string target_device_name) { + if (cubeb_init(&ctx, "yuzu", nullptr) != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "cubeb_init failed"); + return; + } + + if (target_device_name != auto_device_name && !target_device_name.empty()) { + cubeb_device_collection collection; + if (cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &collection) != CUBEB_OK) { + LOG_WARNING(Audio_Sink, "Audio output device enumeration not supported"); + } else { + const auto collection_end{collection.device + collection.count}; + const auto device{std::find_if(collection.device, collection_end, + [&](const cubeb_device_info& device) { + return target_device_name == device.friendly_name; + })}; + if (device != collection_end) { + output_device = device->devid; + } + cubeb_device_collection_destroy(ctx, &collection); + } + } +} + +CubebSink::~CubebSink() { + if (!ctx) { + return; + } + + for (auto& sink_stream : sink_streams) { + sink_stream.reset(); + } + + cubeb_destroy(ctx); +} + +SinkStream& CubebSink::AcquireSinkStream(u32 sample_rate, u32 num_channels) { + sink_streams.push_back(std::make_unique(ctx, output_device)); + return *sink_streams.back(); +} + +long SinkStreamImpl::DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer, + void* output_buffer, long num_frames) { + SinkStreamImpl* impl = static_cast(user_data); + u8* buffer = reinterpret_cast(output_buffer); + + if (!impl) { + return {}; + } + + const size_t frames_to_write{ + std::min(impl->queue.size() / impl->GetNumChannels(), static_cast(num_frames))}; + + memcpy(buffer, impl->queue.data(), frames_to_write * sizeof(s16) * impl->GetNumChannels()); + impl->queue.erase(impl->queue.begin(), + impl->queue.begin() + frames_to_write * impl->GetNumChannels()); + + if (frames_to_write < num_frames) { + // Fill the rest of the frames with silence + memset(buffer + frames_to_write * sizeof(s16) * impl->GetNumChannels(), 0, + (num_frames - frames_to_write) * sizeof(s16) * impl->GetNumChannels()); + } + + return num_frames; +} + +void SinkStreamImpl::StateCallback(cubeb_stream* stream, void* user_data, cubeb_state state) {} + +std::vector ListCubebSinkDevices() { + std::vector device_list; + cubeb* ctx; + + if (cubeb_init(&ctx, "Citra Device Enumerator", nullptr) != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "cubeb_init failed"); + return {}; + } + + cubeb_device_collection collection; + if (cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &collection) != CUBEB_OK) { + LOG_WARNING(Audio_Sink, "Audio output device enumeration not supported"); + } else { + for (size_t i = 0; i < collection.count; i++) { + const cubeb_device_info& device = collection.device[i]; + if (device.friendly_name) { + device_list.emplace_back(device.friendly_name); + } + } + cubeb_device_collection_destroy(ctx, &collection); + } + + cubeb_destroy(ctx); + return device_list; +} + +} // namespace AudioCore diff --git a/src/audio_core/cubeb_sink.h b/src/audio_core/cubeb_sink.h new file mode 100644 index 0000000000..d07113f1fa --- /dev/null +++ b/src/audio_core/cubeb_sink.h @@ -0,0 +1,31 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include + +#include + +#include "audio_core/sink.h" + +namespace AudioCore { + +class CubebSink final : public Sink { +public: + explicit CubebSink(std::string device_id); + ~CubebSink() override; + + SinkStream& AcquireSinkStream(u32 sample_rate, u32 num_channels) override; + +private: + cubeb* ctx{}; + cubeb_devid output_device{}; + std::vector sink_streams; +}; + +std::vector ListCubebSinkDevices(); + +} // namespace AudioCore diff --git a/src/audio_core/sink_details.cpp b/src/audio_core/sink_details.cpp index 6396d80654..955ba20fb4 100644 --- a/src/audio_core/sink_details.cpp +++ b/src/audio_core/sink_details.cpp @@ -8,12 +8,18 @@ #include #include "audio_core/null_sink.h" #include "audio_core/sink_details.h" +#ifdef HAVE_CUBEB +#include "audio_core/cubeb_sink.h" +#endif #include "common/logging/log.h" namespace AudioCore { // g_sink_details is ordered in terms of desirability, with the best choice at the top. const std::vector g_sink_details = { +#ifdef HAVE_CUBEB + SinkDetails{"cubeb", &std::make_unique, &ListCubebSinkDevices}, +#endif SinkDetails{"null", &std::make_unique, [] { return std::vector{"null"}; }}, }; diff --git a/src/audio_core/stream.cpp b/src/audio_core/stream.cpp index 63edc6c8da..689f51a1d7 100644 --- a/src/audio_core/stream.cpp +++ b/src/audio_core/stream.cpp @@ -7,6 +7,8 @@ #include "core/core_timing.h" #include "core/core_timing_util.h" +#include "audio_core/sink.h" +#include "audio_core/sink_details.h" #include "audio_core/stream.h" namespace AudioCore { @@ -31,6 +33,11 @@ u32 Stream::GetSampleSize() const { return GetNumChannels() * 2; } +Stream::Stream(u32 sample_rate, Format format, ReleaseCallback&& release_callback, + SinkStream& sink_stream) + : sample_rate{sample_rate}, format{format}, release_callback{std::move(release_callback)}, + sink_stream{sink_stream} { + release_event = CoreTiming::RegisterEvent( "Stream::Release", [this](u64 userdata, int cycles_late) { ReleaseActiveBuffer(); }); } @@ -68,6 +75,10 @@ void Stream::PlayNextBuffer() { active_buffer = queued_buffers.front(); queued_buffers.pop(); + sink_stream.EnqueueSamples(GetNumChannels(), + reinterpret_cast(active_buffer->GetData().data()), + active_buffer->GetData().size() / GetSampleSize()); + CoreTiming::ScheduleEventThreadsafe(GetBufferReleaseCycles(*active_buffer), release_event, {}); } diff --git a/src/audio_core/stream.h b/src/audio_core/stream.h index 5c10058998..35253920e6 100644 --- a/src/audio_core/stream.h +++ b/src/audio_core/stream.h @@ -10,6 +10,7 @@ #include #include "audio_core/buffer.h" +#include "audio_core/sink_stream.h" #include "common/assert.h" #include "common/common_types.h" #include "core/core_timing.h" @@ -31,7 +32,8 @@ public: /// Callback function type, used to change guest state on a buffer being released using ReleaseCallback = std::function; - Stream(int sample_rate, Format format, ReleaseCallback&& release_callback); + Stream(u32 sample_rate, Format format, ReleaseCallback&& release_callback, + SinkStream& sink_stream); /// Plays the audio stream void Play(); @@ -85,7 +87,7 @@ private: /// 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 + u32 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 @@ -93,6 +95,7 @@ private: BufferPtr active_buffer; ///< Actively playing buffer in the stream std::queue queued_buffers; ///< Buffers queued to be played in the stream std::queue released_buffers; ///< Buffers recently released from the stream + SinkStream& sink_stream; ///< Output sink for the stream }; using StreamPtr = std::shared_ptr;