mirror of
				https://git.tardis.systems/mirrors/yuzu
				synced 2025-10-31 10:44:49 +01:00 
			
		
		
		
	audio_core: Implement Sink and SinkStream interfaces with cubeb.
This commit is contained in:
		
							parent
							
								
									9ef227e09d
								
							
						
					
					
						commit
						f437c11caf
					
				| @ -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 | ||||
|  | ||||
							
								
								
									
										6
									
								
								externals/CMakeLists.txt
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								externals/CMakeLists.txt
									
									
									
									
										vendored
									
									
								
							| @ -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() | ||||
|  | ||||
| @ -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() | ||||
|  | ||||
| @ -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<Stream>(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<Stream>(sample_rate, ChannelsToStreamFormat(num_channels), | ||||
|                                     std::move(release_callback), | ||||
|                                     sink->AcquireSinkStream(sample_rate, num_channels)); | ||||
| } | ||||
| 
 | ||||
| std::vector<u64> AudioOut::GetTagsAndReleaseBuffers(StreamPtr stream, size_t max_count) { | ||||
|  | ||||
| @ -8,6 +8,7 @@ | ||||
| #include <vector> | ||||
| 
 | ||||
| #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<StreamPtr> streams; | ||||
| }; | ||||
| 
 | ||||
| } // namespace AudioCore
 | ||||
|  | ||||
							
								
								
									
										190
									
								
								src/audio_core/cubeb_sink.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								src/audio_core/cubeb_sink.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,190 @@ | ||||
| // Copyright 2018 yuzu Emulator Project
 | ||||
| // Licensed under GPLv2 or any later version
 | ||||
| // Refer to the license.txt file included.
 | ||||
| 
 | ||||
| #include <algorithm> | ||||
| #include <cstring> | ||||
| 
 | ||||
| #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<std::string> device_list; | ||||
| 
 | ||||
|     cubeb* ctx{}; | ||||
|     cubeb_stream* stream_backend{}; | ||||
| 
 | ||||
|     std::vector<s16> 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<SinkStreamImpl>(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<SinkStreamImpl*>(user_data); | ||||
|     u8* buffer = reinterpret_cast<u8*>(output_buffer); | ||||
| 
 | ||||
|     if (!impl) { | ||||
|         return {}; | ||||
|     } | ||||
| 
 | ||||
|     const size_t frames_to_write{ | ||||
|         std::min(impl->queue.size() / impl->GetNumChannels(), static_cast<size_t>(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<std::string> ListCubebSinkDevices() { | ||||
|     std::vector<std::string> 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
 | ||||
							
								
								
									
										31
									
								
								src/audio_core/cubeb_sink.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/audio_core/cubeb_sink.h
									
									
									
									
									
										Normal file
									
								
							| @ -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 <string> | ||||
| #include <vector> | ||||
| 
 | ||||
| #include <cubeb/cubeb.h> | ||||
| 
 | ||||
| #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<SinkStreamPtr> sink_streams; | ||||
| }; | ||||
| 
 | ||||
| std::vector<std::string> ListCubebSinkDevices(); | ||||
| 
 | ||||
| } // namespace AudioCore
 | ||||
| @ -8,12 +8,18 @@ | ||||
| #include <vector> | ||||
| #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<SinkDetails> g_sink_details = { | ||||
| #ifdef HAVE_CUBEB | ||||
|     SinkDetails{"cubeb", &std::make_unique<CubebSink, std::string>, &ListCubebSinkDevices}, | ||||
| #endif | ||||
|     SinkDetails{"null", &std::make_unique<NullSink, std::string>, | ||||
|                 [] { return std::vector<std::string>{"null"}; }}, | ||||
| }; | ||||
|  | ||||
| @ -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<const s16*>(active_buffer->GetData().data()), | ||||
|                                active_buffer->GetData().size() / GetSampleSize()); | ||||
| 
 | ||||
|     CoreTiming::ScheduleEventThreadsafe(GetBufferReleaseCycles(*active_buffer), release_event, {}); | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -10,6 +10,7 @@ | ||||
| #include <queue> | ||||
| 
 | ||||
| #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<void()>; | ||||
| 
 | ||||
|     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<BufferPtr> queued_buffers;   ///< Buffers queued to be played in the stream
 | ||||
|     std::queue<BufferPtr> released_buffers; ///< Buffers recently released from the stream
 | ||||
|     SinkStream& sink_stream;                ///< Output sink for the stream
 | ||||
| }; | ||||
| 
 | ||||
| using StreamPtr = std::shared_ptr<Stream>; | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user