audio_core: Implement Sink and SinkStream interfaces with cubeb.
This commit is contained in:
		@@ -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>;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user