Skip to content
Snippets Groups Projects
Unverified Commit bf9c62bc authored by bunnei's avatar bunnei Committed by GitHub
Browse files

Merge pull request #855 from bunnei/cubeb

Audio output backend based on cubeb
parents 420f8fb2 f437c11c
No related branches found
No related tags found
No related merge requests found
Showing
with 472 additions and 39 deletions
......@@ -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
......
......@@ -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
......
......@@ -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()
Subproject commit 12b78c0edfa40007e41dbdcd9dfe367fbb98d018
......@@ -2,10 +2,22 @@ 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
sink.h
sink_details.cpp
sink_details.h
sink_stream.h
)
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,13 +3,15 @@
// 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"
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,11 +26,16 @@ 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<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,20 +8,19 @@
#include <vector>
#include "audio_core/buffer.h"
#include "audio_core/sink.h"
#include "audio_core/stream.h"
#include "common/common_types.h"
namespace AudioCore {
using StreamPtr = std::shared_ptr<Stream>;
/**
* 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,8 +36,7 @@ public:
bool QueueBuffer(StreamPtr stream, Buffer::Tag tag, std::vector<u8>&& data);
private:
/// Active audio streams on the interface
std::vector<StreamPtr> streams;
SinkPtr sink;
};
} // namespace AudioCore
......@@ -4,6 +4,7 @@
#pragma once
#include <memory>
#include <vector>
#include "common/common_types.h"
......@@ -34,4 +35,6 @@ private:
std::vector<u8> data;
};
using BufferPtr = std::shared_ptr<Buffer>;
} // namespace AudioCore
// 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, &params, &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, &params, 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
// 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
// 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
// Copyright 2018 yuzu Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <memory>
#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<Sink>;
} // namespace AudioCore
// Copyright 2018 yuzu Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <algorithm>
#include <memory>
#include <string>
#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"}; }},
};
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
// Copyright 2018 yuzu 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>(std::string)> factory_,
std::function<std::vector<std::string>()> 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::unique_ptr<Sink>(std::string device_id)> factory;
/// A method to call to list available devices.
std::function<std::vector<std::string>()> list_devices;
};
extern const std::vector<SinkDetails> g_sink_details;
const SinkDetails& GetSinkDetails(std::string sink_id);
} // namespace AudioCore
// Copyright 2018 yuzu Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <memory>
#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<SinkStream>;
} // namespace AudioCore
......@@ -7,30 +7,37 @@
#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 {
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<u32>(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;
}
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(); });
}
......@@ -45,7 +52,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<u64>(num_samples) * 1000000) / sample_rate);
}
......@@ -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,14 +10,13 @@
#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"
namespace AudioCore {
using BufferPtr = std::shared_ptr<Buffer>;
/**
* Represents an audio stream, which is a sequence of queued buffers, to be outputed by AudioOut
*/
......@@ -33,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();
......@@ -60,6 +60,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 {
......@@ -76,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
......@@ -84,6 +95,9 @@ 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>;
} // namespace AudioCore
......@@ -177,7 +177,6 @@ System::ResultStatus System::Init(EmuWindow* emu_window, u32 system_mode) {
}
gpu_core = std::make_unique<Tegra::GPU>();
audio_core = std::make_unique<AudioCore::AudioOut>();
telemetry_session = std::make_unique<Core::TelemetrySession>();
service_manager = std::make_shared<Service::SM::ServiceManager>();
......@@ -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();
......
......@@ -8,7 +8,6 @@
#include <memory>
#include <string>
#include <thread>
#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<Loader::AppLoader> app_loader;
std::unique_ptr<Tegra::GPU> gpu_core;
std::unique_ptr<AudioCore::AudioOut> audio_core;
std::shared_ptr<Tegra::DebugContext> debug_context;
Kernel::SharedPtr<Kernel::Process> current_process;
std::shared_ptr<ExclusiveMonitor> cpu_exclusive_monitor;
......
......@@ -25,9 +25,8 @@ constexpr int DefaultSampleRate{48000};
class IAudioOut final : public ServiceFramework<IAudioOut> {
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<IAudioOut>(std::move(params));
audio_out_interface = std::make_shared<IAudioOut>(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<AudioCore::AudioOut>();
}
} // namespace Service::Audio
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment