committed by
GitHub
21 changed files with 474 additions and 39 deletions
-
3.gitmodules
-
2CMakeLists.txt
-
6externals/CMakeLists.txt
-
1externals/cubeb
-
12src/audio_core/CMakeLists.txt
-
17src/audio_core/audio_out.cpp
-
8src/audio_core/audio_out.h
-
3src/audio_core/buffer.h
-
190src/audio_core/cubeb_sink.cpp
-
31src/audio_core/cubeb_sink.h
-
27src/audio_core/null_sink.h
-
29src/audio_core/sink.h
-
44src/audio_core/sink_details.cpp
-
32src/audio_core/sink_details.h
-
32src/audio_core/sink_stream.h
-
35src/audio_core/stream.cpp
-
22src/audio_core/stream.h
-
2src/core/core.cpp
-
7src/core/core.h
-
8src/core/hle/service/audio/audout_u.cpp
-
2src/core/hle/service/audio/audout_u.h
@ -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
|
||||
@ -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 |
||||
@ -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 |
||||
@ -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 <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 |
||||
@ -0,0 +1,44 @@ |
|||||
|
// 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
|
||||
@ -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 <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 |
||||
@ -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 <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 |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue