mirror of
https://git.suyu.dev/suyu/suyu.git
synced 2024-11-25 16:22:47 +01:00
audio_core: Implement Sink and SinkStream interfaces with cubeb.
This commit is contained in:
parent
9ef227e09d
commit
f437c11caf
10 changed files with 269 additions and 6 deletions
|
@ -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(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)
|
if(NOT EXISTS ${CMAKE_SOURCE_DIR}/.git/hooks/pre-commit)
|
||||||
message(STATUS "Copying pre-commit hook")
|
message(STATUS "Copying pre-commit hook")
|
||||||
file(COPY hooks/pre-commit
|
file(COPY hooks/pre-commit
|
||||||
|
|
6
externals/CMakeLists.txt
vendored
6
externals/CMakeLists.txt
vendored
|
@ -54,3 +54,9 @@ endif()
|
||||||
# Opus
|
# Opus
|
||||||
add_subdirectory(opus)
|
add_subdirectory(opus)
|
||||||
target_include_directories(opus INTERFACE ./opus/include)
|
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.cpp
|
||||||
audio_out.h
|
audio_out.h
|
||||||
buffer.h
|
buffer.h
|
||||||
|
cubeb_sink.cpp
|
||||||
|
cubeb_sink.h
|
||||||
null_sink.h
|
null_sink.h
|
||||||
stream.cpp
|
stream.cpp
|
||||||
stream.h
|
stream.h
|
||||||
|
@ -14,3 +16,8 @@ add_library(audio_core STATIC
|
||||||
create_target_directory_groups(audio_core)
|
create_target_directory_groups(audio_core)
|
||||||
|
|
||||||
target_link_libraries(audio_core PUBLIC common 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.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
#include "audio_core/audio_out.h"
|
#include "audio_core/audio_out.h"
|
||||||
|
#include "audio_core/sink.h"
|
||||||
|
#include "audio_core/sink_details.h"
|
||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
#include "common/logging/log.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,
|
StreamPtr AudioOut::OpenStream(u32 sample_rate, u32 num_channels,
|
||||||
Stream::ReleaseCallback&& release_callback) {
|
Stream::ReleaseCallback&& release_callback) {
|
||||||
streams.push_back(std::make_shared<Stream>(sample_rate, ChannelsToStreamFormat(num_channels),
|
if (!sink) {
|
||||||
std::move(release_callback)));
|
const SinkDetails& sink_details = GetSinkDetails("auto");
|
||||||
return streams.back();
|
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) {
|
std::vector<u64> AudioOut::GetTagsAndReleaseBuffers(StreamPtr stream, size_t max_count) {
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "audio_core/buffer.h"
|
#include "audio_core/buffer.h"
|
||||||
|
#include "audio_core/sink.h"
|
||||||
#include "audio_core/stream.h"
|
#include "audio_core/stream.h"
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
|
|
||||||
|
@ -36,7 +37,6 @@ public:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
SinkPtr sink;
|
SinkPtr sink;
|
||||||
std::vector<StreamPtr> streams;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace AudioCore
|
} // 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 <vector>
|
||||||
#include "audio_core/null_sink.h"
|
#include "audio_core/null_sink.h"
|
||||||
#include "audio_core/sink_details.h"
|
#include "audio_core/sink_details.h"
|
||||||
|
#ifdef HAVE_CUBEB
|
||||||
|
#include "audio_core/cubeb_sink.h"
|
||||||
|
#endif
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
|
|
||||||
namespace AudioCore {
|
namespace AudioCore {
|
||||||
|
|
||||||
// g_sink_details is ordered in terms of desirability, with the best choice at the top.
|
// g_sink_details is ordered in terms of desirability, with the best choice at the top.
|
||||||
const std::vector<SinkDetails> g_sink_details = {
|
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>,
|
SinkDetails{"null", &std::make_unique<NullSink, std::string>,
|
||||||
[] { return std::vector<std::string>{"null"}; }},
|
[] { return std::vector<std::string>{"null"}; }},
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
#include "core/core_timing.h"
|
#include "core/core_timing.h"
|
||||||
#include "core/core_timing_util.h"
|
#include "core/core_timing_util.h"
|
||||||
|
|
||||||
|
#include "audio_core/sink.h"
|
||||||
|
#include "audio_core/sink_details.h"
|
||||||
#include "audio_core/stream.h"
|
#include "audio_core/stream.h"
|
||||||
|
|
||||||
namespace AudioCore {
|
namespace AudioCore {
|
||||||
|
@ -31,6 +33,11 @@ u32 Stream::GetSampleSize() const {
|
||||||
return GetNumChannels() * 2;
|
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(
|
release_event = CoreTiming::RegisterEvent(
|
||||||
"Stream::Release", [this](u64 userdata, int cycles_late) { ReleaseActiveBuffer(); });
|
"Stream::Release", [this](u64 userdata, int cycles_late) { ReleaseActiveBuffer(); });
|
||||||
}
|
}
|
||||||
|
@ -68,6 +75,10 @@ void Stream::PlayNextBuffer() {
|
||||||
active_buffer = queued_buffers.front();
|
active_buffer = queued_buffers.front();
|
||||||
queued_buffers.pop();
|
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, {});
|
CoreTiming::ScheduleEventThreadsafe(GetBufferReleaseCycles(*active_buffer), release_event, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
#include <queue>
|
#include <queue>
|
||||||
|
|
||||||
#include "audio_core/buffer.h"
|
#include "audio_core/buffer.h"
|
||||||
|
#include "audio_core/sink_stream.h"
|
||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
#include "core/core_timing.h"
|
#include "core/core_timing.h"
|
||||||
|
@ -31,7 +32,8 @@ public:
|
||||||
/// Callback function type, used to change guest state on a buffer being released
|
/// Callback function type, used to change guest state on a buffer being released
|
||||||
using ReleaseCallback = std::function<void()>;
|
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
|
/// Plays the audio stream
|
||||||
void Play();
|
void Play();
|
||||||
|
@ -85,7 +87,7 @@ private:
|
||||||
/// Gets the number of core cycles when the specified buffer will be released
|
/// Gets the number of core cycles when the specified buffer will be released
|
||||||
s64 GetBufferReleaseCycles(const Buffer& buffer) const;
|
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
|
Format format; ///< Format of the stream
|
||||||
ReleaseCallback release_callback; ///< Buffer release callback for the stream
|
ReleaseCallback release_callback; ///< Buffer release callback for the stream
|
||||||
State state{State::Stopped}; ///< Playback state of 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
|
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> queued_buffers; ///< Buffers queued to be played in the stream
|
||||||
std::queue<BufferPtr> released_buffers; ///< Buffers recently released from 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>;
|
using StreamPtr = std::shared_ptr<Stream>;
|
||||||
|
|
Loading…
Reference in a new issue