Audio: device switching and channel count detection (#12246)

This commit is contained in:
Vestrel
2022-07-08 17:13:38 +02:00
committed by GitHub
parent 4b787b22c8
commit 98b730c806
39 changed files with 1235 additions and 395 deletions
+9 -3
View File
@@ -5,6 +5,8 @@
#include "util/logs.hpp"
#include "util/v128.hpp"
#include <locale>
#include <codecvt>
#include <algorithm>
#include <string_view>
#include "Thread.h"
@@ -13,8 +15,6 @@
#include <Windows.h>
#else
#include <errno.h>
#include <locale>
#include <codecvt>
#endif
std::string wchar_to_utf8(std::wstring_view src)
@@ -26,11 +26,17 @@ std::string wchar_to_utf8(std::wstring_view src)
WideCharToMultiByte(CP_UTF8, 0, src.data(), src.size(), utf8_string.data(), tmp_size, nullptr, nullptr);
return utf8_string;
#else
std::wstring_convert<std::codecvt_utf8_utf16<wchar_t>, wchar_t> converter{};
std::wstring_convert<std::codecvt_utf8<wchar_t>, wchar_t> converter{};
return converter.to_bytes(src.data());
#endif
}
std::string utf16_to_utf8(std::u16string_view src)
{
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> converter{};
return converter.to_bytes(src.data());
}
std::wstring utf8_to_wchar(std::string_view src)
{
#ifdef _WIN32
+1
View File
@@ -10,6 +10,7 @@
std::wstring utf8_to_wchar(std::string_view src);
std::string wchar_to_utf8(std::wstring_view src);
std::string utf16_to_utf8(std::u16string_view src);
// Copy null-terminated string from a std::string or a char array to a char array with truncation
template <typename D, typename T>
+3 -1
View File
@@ -53,11 +53,13 @@
</ItemGroup>
<ItemGroup>
<ClInclude Include="Emu\Audio\Cubeb\CubebBackend.h" />
<ClInclude Include="Emu\Audio\Cubeb\cubeb_enumerator.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="Emu\Audio\Cubeb\CubebBackend.cpp" />
<ClCompile Include="Emu\Audio\Cubeb\cubeb_enumerator.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
</Project>
+6
View File
@@ -10,10 +10,16 @@
<ClCompile Include="Emu\Audio\Cubeb\CubebBackend.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Emu\Audio\Cubeb\cubeb_enumerator.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Emu\Audio\Cubeb\CubebBackend.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="Emu\Audio\Cubeb\cubeb_enumerator.h">
<Filter>Source Files</Filter>
</ClInclude>
</ItemGroup>
</Project>
+30 -3
View File
@@ -6,10 +6,16 @@
AudioBackend::AudioBackend() {}
void AudioBackend::SetErrorCallback(std::function<void()> cb)
void AudioBackend::SetWriteCallback(std::function<u32(u32 /* byte_cnt */, void* /* buffer */)> cb)
{
std::lock_guard lock(m_error_cb_mutex);
m_error_callback = cb;
std::lock_guard lock(m_cb_mutex);
m_write_callback = cb;
}
void AudioBackend::SetStateCallback(std::function<void(AudioStateEvent)> cb)
{
std::lock_guard lock(m_state_cb_mutex);
m_state_callback = cb;
}
/*
@@ -134,3 +140,24 @@ AudioChannelCnt AudioBackend::get_max_channel_count(u32 device_index)
return count;
}
AudioChannelCnt AudioBackend::convert_channel_count(u64 raw)
{
switch (raw)
{
default:
case 8:
return AudioChannelCnt::SURROUND_7_1;
case 7:
case 6:
return AudioChannelCnt::SURROUND_5_1;
case 5:
case 4:
case 3:
case 2:
case 1:
return AudioChannelCnt::STEREO;
case 0:
fmt::throw_exception("Usupported channel count");
}
}
+27 -6
View File
@@ -37,6 +37,12 @@ enum class AudioChannelCnt : u32
SURROUND_7_1 = 8,
};
enum class AudioStateEvent : u32
{
UNSPECIFIED_ERROR,
DEFAULT_DEVICE_CHANGED,
};
class AudioBackend
{
public:
@@ -60,19 +66,21 @@ public:
virtual std::string_view GetName() const = 0;
// (Re)create output stream with new parameters. Blocks until data callback returns.
// If dev_id is empty, then default device will be selected.
// May override channel count if device has smaller number of channels.
// Should return 'true' on success.
virtual bool Open(AudioFreq freq, AudioSampleSize sample_size, AudioChannelCnt ch_cnt) = 0;
virtual bool Open(std::string_view dev_id, AudioFreq freq, AudioSampleSize sample_size, AudioChannelCnt ch_cnt) = 0;
// Reset backend state. Blocks until data callback returns.
virtual void Close() = 0;
// Sets write callback. It's called when backend requests new data to be sent.
// Callback should return number of submitted bytes. Calling other backend functions from callback is unsafe.
virtual void SetWriteCallback(std::function<u32(u32 /* byte_cnt */, void* /* buffer */)> cb) = 0;
virtual void SetWriteCallback(std::function<u32(u32 /* byte_cnt */, void* /* buffer */)> cb);
// Sets error callback. It's called when backend detects uncorrectable error condition in audio chain.
// Sets error callback. It's called when backend detects event in audio chain that needs immediate attention.
// Calling other backend functions from callback is unsafe.
virtual void SetErrorCallback(std::function<void()> cb);
virtual void SetStateCallback(std::function<void(AudioStateEvent)> cb);
/*
* All functions below require that Open() was called prior.
@@ -101,6 +109,11 @@ public:
*/
virtual bool Operational() { return true; }
/*
* This virtual method should be reimplemented if backend can report device changes
*/
virtual bool DefaultDeviceChanged() { return false; }
/*
* Helper methods
*/
@@ -145,6 +158,11 @@ public:
*/
static AudioChannelCnt get_max_channel_count(u32 device_index);
/*
* Converts raw channel count to value usable by backends
*/
static AudioChannelCnt convert_channel_count(u64 raw);
/*
* Downmix audio stream.
*/
@@ -208,8 +226,11 @@ protected:
AudioFreq m_sampling_rate = AudioFreq::FREQ_48K;
AudioChannelCnt m_channels = AudioChannelCnt::STEREO;
shared_mutex m_error_cb_mutex{};
std::function<void()> m_error_callback{};
shared_mutex m_cb_mutex{};
std::function<u32(u32, void *)> m_write_callback{};
shared_mutex m_state_cb_mutex{};
std::function<void(AudioStateEvent)> m_state_callback{};
bool m_playing = false;
+239 -37
View File
@@ -2,6 +2,7 @@
#include <algorithm>
#include "util/logs.hpp"
#include "Emu/Audio/audio_device_enumerator.h"
#ifdef _WIN32
#include <Windows.h>
@@ -15,8 +16,7 @@ CubebBackend::CubebBackend()
{
#ifdef _WIN32
// Cubeb requires COM to be initialized on the thread calling cubeb_init on Windows
HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
if (SUCCEEDED(hr))
if (HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); SUCCEEDED(hr))
{
m_com_init_success = true;
}
@@ -24,11 +24,20 @@ CubebBackend::CubebBackend()
if (int err = cubeb_init(&m_ctx, "RPCS3", nullptr))
{
Cubeb.error("cubeb_init() failed: 0x%08x", err);
Cubeb.error("cubeb_init() failed: %i", err);
m_ctx = nullptr;
return;
}
if (int err = cubeb_register_device_collection_changed(m_ctx, CUBEB_DEVICE_TYPE_OUTPUT, device_collection_changed_cb, this))
{
Cubeb.error("cubeb_register_device_collection_changed() failed: %i", err);
}
else
{
m_dev_collection_cb_enabled = true;
}
Cubeb.notice("Using backend %s", cubeb_get_backend_id(m_ctx));
}
@@ -36,6 +45,14 @@ CubebBackend::~CubebBackend()
{
Close();
if (m_dev_collection_cb_enabled)
{
if (int err = cubeb_register_device_collection_changed(m_ctx, CUBEB_DEVICE_TYPE_OUTPUT, nullptr, nullptr))
{
Cubeb.error("cubeb_register_device_collection_changed() failed: %i", err);
}
}
if (m_ctx)
{
cubeb_destroy(m_ctx);
@@ -56,11 +73,16 @@ bool CubebBackend::Initialized()
bool CubebBackend::Operational()
{
std::lock_guard lock(m_error_cb_mutex);
return m_stream != nullptr && !m_reset_req;
return m_stream != nullptr && !m_reset_req.observe();
}
bool CubebBackend::Open(AudioFreq freq, AudioSampleSize sample_size, AudioChannelCnt ch_cnt)
bool CubebBackend::DefaultDeviceChanged()
{
std::lock_guard lock{m_dev_sw_mutex};
return !m_reset_req.observe() && m_default_dev_changed;
}
bool CubebBackend::Open(std::string_view dev_id, AudioFreq freq, AudioSampleSize sample_size, AudioChannelCnt ch_cnt)
{
if (!Initialized())
{
@@ -69,11 +91,39 @@ bool CubebBackend::Open(AudioFreq freq, AudioSampleSize sample_size, AudioChanne
}
std::lock_guard lock(m_cb_mutex);
std::lock_guard dev_sw_lock{m_dev_sw_mutex};
CloseUnlocked();
const bool use_default_device = dev_id.empty() || dev_id == audio_device_enumerator::DEFAULT_DEV_ID;
auto [dev_handle, dev_ident, dev_ch_cnt] = GetDevice(use_default_device ? "" : dev_id);
if (!dev_handle)
{
if (use_default_device)
{
std::tie(dev_handle, dev_ident, dev_ch_cnt) = GetDefaultDeviceAlt(freq, sample_size, ch_cnt);
if (!dev_handle)
{
Cubeb.error("Cannot detect default device. Channel count detection unavailable.");
}
}
else
{
Cubeb.error("Device with id=%s not found", dev_id);
return false;
}
}
if (dev_ch_cnt == 0)
{
Cubeb.error("Device reported invalid channel count, using stereo instead");
dev_ch_cnt = 2;
}
m_sampling_rate = freq;
m_sample_size = sample_size;
m_channels = ch_cnt;
m_channels = static_cast<AudioChannelCnt>(std::min(static_cast<u32>(convert_channel_count(dev_ch_cnt)), static_cast<u32>(ch_cnt)));
full_sample_size = get_channels() * get_sample_size();
cubeb_stream_params stream_param{};
@@ -82,46 +132,50 @@ bool CubebBackend::Open(AudioFreq freq, AudioSampleSize sample_size, AudioChanne
stream_param.channels = get_channels();
stream_param.layout = [&]()
{
switch (ch_cnt)
switch (m_channels)
{
case AudioChannelCnt::STEREO: return CUBEB_LAYOUT_STEREO;
case AudioChannelCnt::SURROUND_5_1: return CUBEB_LAYOUT_3F2_LFE;
case AudioChannelCnt::SURROUND_7_1: return CUBEB_LAYOUT_3F4_LFE;
default:
ensure(false);
return CUBEB_LAYOUT_UNDEFINED;
fmt::throw_exception("Invalid audio channel count");
}
}();
stream_param.prefs = m_dev_collection_cb_enabled && dev_handle ? CUBEB_STREAM_PREF_DISABLE_DEVICE_SWITCHING : CUBEB_STREAM_PREF_NONE;
u32 min_latency{};
if (int err = cubeb_get_min_latency(m_ctx, &stream_param, &min_latency))
{
Cubeb.error("cubeb_get_min_latency() failed: 0x%08x", err);
Cubeb.error("cubeb_get_min_latency() failed: %i", err);
min_latency = 0;
}
const u32 stream_latency = std::max(static_cast<u32>(AUDIO_MIN_LATENCY * get_sampling_rate()), min_latency);
if (int err = cubeb_stream_init(m_ctx, &m_stream, "Main stream", nullptr, nullptr, nullptr, &stream_param, stream_latency, data_cb, state_cb, this))
if (int err = cubeb_stream_init(m_ctx, &m_stream, "Main stream", nullptr, nullptr, dev_handle, &stream_param, stream_latency, data_cb, state_cb, this))
{
Cubeb.error("cubeb_stream_init() failed: 0x%08x", err);
Cubeb.error("cubeb_stream_init() failed: %i", err);
m_stream = nullptr;
}
else if (int err = cubeb_stream_start(m_stream))
{
Cubeb.error("cubeb_stream_start() failed: 0x%08x", err);
CloseUnlocked();
}
else if (int err = cubeb_stream_set_volume(m_stream, 1.0))
{
Cubeb.error("cubeb_stream_set_volume() failed: 0x%08x", err);
return false;
}
if (m_stream == nullptr)
if (int err = cubeb_stream_start(m_stream))
{
Cubeb.error("Failed to open audio backend. Make sure that no other application is running that might block audio access (e.g. Netflix).");
Cubeb.error("cubeb_stream_start() failed: %i", err);
CloseUnlocked();
return false;
}
if (int err = cubeb_stream_set_volume(m_stream, 1.0))
{
Cubeb.error("cubeb_stream_set_volume() failed: %i", err);
}
if (use_default_device)
{
m_current_device = dev_ident;
}
return true;
}
@@ -131,7 +185,7 @@ void CubebBackend::CloseUnlocked()
{
if (int err = cubeb_stream_stop(m_stream))
{
Cubeb.error("cubeb_stream_stop() failed: 0x%08x", err);
Cubeb.error("cubeb_stream_stop() failed: %i", err);
}
cubeb_stream_destroy(m_stream);
@@ -140,11 +194,15 @@ void CubebBackend::CloseUnlocked()
m_playing = false;
m_last_sample.fill(0);
m_default_dev_changed = false;
m_current_device.clear();
}
void CubebBackend::Close()
{
std::lock_guard lock(m_cb_mutex);
std::lock_guard dev_sw_lock{m_dev_sw_mutex};
CloseUnlocked();
}
@@ -177,12 +235,6 @@ void CubebBackend::Pause()
m_last_sample.fill(0);
}
void CubebBackend::SetWriteCallback(std::function<u32(u32, void *)> cb)
{
std::lock_guard lock(m_cb_mutex);
m_write_callback = cb;
}
f64 CubebBackend::GetCallbackFrameLen()
{
if (m_stream == nullptr)
@@ -194,19 +246,130 @@ f64 CubebBackend::GetCallbackFrameLen()
u32 stream_latency{};
if (int err = cubeb_stream_get_latency(m_stream, &stream_latency))
{
Cubeb.error("cubeb_stream_get_latency() failed: 0x%08x", err);
Cubeb.error("cubeb_stream_get_latency() failed: %i", err);
stream_latency = 0;
}
return std::max<f64>(AUDIO_MIN_LATENCY, static_cast<f64>(stream_latency) / get_sampling_rate());
}
std::tuple<cubeb_devid, std::string, u32> CubebBackend::GetDevice(std::string_view dev_id)
{
const bool default_dev = dev_id.empty();
cubeb_device_collection dev_collection{};
if (int err = cubeb_enumerate_devices(m_ctx, CUBEB_DEVICE_TYPE_OUTPUT, &dev_collection))
{
Cubeb.error("cubeb_enumerate_devices() failed: %i", err);
return {};
}
if (dev_collection.count == 0)
{
Cubeb.error("No output devices available");
if (int err = cubeb_device_collection_destroy(m_ctx, &dev_collection))
{
Cubeb.error("cubeb_device_collection_destroy() failed: %i", err);
}
return {};
}
std::tuple<cubeb_devid, std::string, u32> result{};
for (u64 dev_idx = 0; dev_idx < dev_collection.count; dev_idx++)
{
const cubeb_device_info& dev_info = dev_collection.device[dev_idx];
const std::string dev_ident{dev_info.device_id};
if (dev_ident.empty())
{
Cubeb.error("device_id is missing from device");
continue;
}
if (default_dev)
{
if (dev_info.preferred & CUBEB_DEVICE_PREF_MULTIMEDIA)
{
result = {dev_info.devid, dev_ident, dev_info.max_channels};
break;
}
}
else if (dev_ident == dev_id)
{
result = {dev_info.devid, dev_ident, dev_info.max_channels};
break;
}
}
if (int err = cubeb_device_collection_destroy(m_ctx, &dev_collection))
{
Cubeb.error("cubeb_device_collection_destroy() failed: %i", err);
}
return result;
};
std::tuple<cubeb_devid, std::string, u32> CubebBackend::GetDefaultDeviceAlt(AudioFreq freq, AudioSampleSize sample_size, AudioChannelCnt ch_cnt)
{
cubeb_stream_params param =
{
.format = sample_size == AudioSampleSize::S16 ? CUBEB_SAMPLE_S16NE : CUBEB_SAMPLE_FLOAT32NE,
.rate = static_cast<u32>(freq),
.channels = static_cast<u32>(ch_cnt),
.layout = CUBEB_LAYOUT_UNDEFINED,
.prefs = CUBEB_STREAM_PREF_DISABLE_DEVICE_SWITCHING
};
u32 min_latency{};
if (int err = cubeb_get_min_latency(m_ctx, &param, &min_latency))
{
Cubeb.error("cubeb_get_min_latency() failed: %i", err);
min_latency = 100;
}
cubeb_stream* tmp_stream{};
static auto dummy_data_cb = [](cubeb_stream*, void*, void const*, void*, long) -> long { return 0; };
static auto dummy_state_cb = [](cubeb_stream*, void*, cubeb_state) {};
if (int err = cubeb_stream_init(m_ctx, &tmp_stream, "Default device detector", nullptr, nullptr, nullptr, &param, min_latency, dummy_data_cb, dummy_state_cb, nullptr))
{
Cubeb.error("cubeb_stream_init() failed: %i", err);
return {};
}
cubeb_device* crnt_dev{};
if (int err = cubeb_stream_get_current_device(tmp_stream, &crnt_dev))
{
Cubeb.error("cubeb_stream_get_current_device() failed: %i", err);
cubeb_stream_destroy(tmp_stream);
return {};
}
const std::string out_dev_name{crnt_dev->output_name};
if (int err = cubeb_stream_device_destroy(tmp_stream, crnt_dev))
{
Cubeb.error("cubeb_stream_device_destroy() failed: %i", err);
}
cubeb_stream_destroy(tmp_stream);
if (out_dev_name.empty())
{
return {};
}
return GetDevice(out_dev_name);
}
long CubebBackend::data_cb(cubeb_stream* /* stream */, void* user_ptr, void const* /* input_buffer */, void* output_buffer, long nframes)
{
CubebBackend* const cubeb = static_cast<CubebBackend*>(user_ptr);
std::unique_lock lock(cubeb->m_cb_mutex, std::defer_lock);
if (nframes && lock.try_lock() && cubeb->m_write_callback && cubeb->m_playing)
if (nframes && !cubeb->m_reset_req.observe() && lock.try_lock() && cubeb->m_write_callback && cubeb->m_playing)
{
const u32 sample_size = cubeb->full_sample_size.observe();
const u32 bytes_req = nframes * sample_size;
@@ -241,12 +404,51 @@ void CubebBackend::state_cb(cubeb_stream* /* stream */, void* user_ptr, cubeb_st
{
Cubeb.error("Stream entered error state");
std::lock_guard lock(cubeb->m_error_cb_mutex);
cubeb->m_reset_req = true;
std::lock_guard lock(cubeb->m_state_cb_mutex);
if (cubeb->m_error_callback)
if (!cubeb->m_reset_req.test_and_set() && cubeb->m_state_callback)
{
cubeb->m_error_callback();
cubeb->m_state_callback(AudioStateEvent::UNSPECIFIED_ERROR);
}
}
}
void CubebBackend::device_collection_changed_cb(cubeb* /* context */, void* user_ptr)
{
CubebBackend* const cubeb = static_cast<CubebBackend*>(user_ptr);
Cubeb.notice("Device collection changed");
std::lock_guard lock{cubeb->m_dev_sw_mutex};
// Non default device is used (or default device cannot be detected)
if (cubeb->m_current_device.empty())
{
return;
}
auto [handle, dev_id, ch_cnt] = cubeb->GetDevice();
if (!handle)
{
std::tie(handle, dev_id, ch_cnt) = cubeb->GetDefaultDeviceAlt(cubeb->m_sampling_rate, cubeb->m_sample_size, cubeb->m_channels);
}
std::lock_guard cb_lock{cubeb->m_state_cb_mutex};
if (!handle)
{
// No devices available
if (!cubeb->m_reset_req.test_and_set() && cubeb->m_state_callback)
{
cubeb->m_state_callback(AudioStateEvent::UNSPECIFIED_ERROR);
}
}
else if (!cubeb->m_reset_req.observe() && dev_id != cubeb->m_current_device)
{
cubeb->m_default_dev_changed = true;
if (cubeb->m_state_callback)
{
cubeb->m_state_callback(AudioStateEvent::DEFAULT_DEVICE_CHANGED);
}
}
}
+13 -5
View File
@@ -20,11 +20,11 @@ public:
bool Initialized() override;
bool Operational() override;
bool DefaultDeviceChanged() override;
bool Open(AudioFreq freq, AudioSampleSize sample_size, AudioChannelCnt ch_cnt) override;
bool Open(std::string_view dev_id, AudioFreq freq, AudioSampleSize sample_size, AudioChannelCnt ch_cnt) override;
void Close() override;
void SetWriteCallback(std::function<u32(u32, void *)> cb) override;
f64 GetCallbackFrameLen() override;
void Play() override;
@@ -39,16 +39,24 @@ private:
bool m_com_init_success = false;
#endif
shared_mutex m_cb_mutex{};
std::function<u32(u32, void *)> m_write_callback{};
std::array<u8, sizeof(float) * static_cast<u32>(AudioChannelCnt::SURROUND_7_1)> m_last_sample{};
atomic_t<u8> full_sample_size = 0;
bool m_reset_req = false;
atomic_t<bool> m_reset_req = false;
shared_mutex m_dev_sw_mutex{};
std::string m_current_device{};
bool m_default_dev_changed = false;
bool m_dev_collection_cb_enabled = false;
// Cubeb callbacks
static long data_cb(cubeb_stream* stream, void* user_ptr, void const* input_buffer, void* output_buffer, long nframes);
static void state_cb(cubeb_stream* stream, void* user_ptr, cubeb_state state);
static void device_collection_changed_cb(cubeb* context, void* user_ptr);
void CloseUnlocked();
std::tuple<cubeb_devid, std::string /* dev_ident */, u32 /* ch_cnt */> GetDevice(std::string_view dev_id = "");
std::tuple<cubeb_devid, std::string /* dev_ident */, u32 /* ch_cnt */> GetDefaultDeviceAlt(AudioFreq freq, AudioSampleSize sample_size, AudioChannelCnt ch_cnt);
};
+113
View File
@@ -0,0 +1,113 @@
#include "Emu/Audio/Cubeb/cubeb_enumerator.h"
#include "util/logs.hpp"
#include <algorithm>
#ifdef _WIN32
#include <Windows.h>
#include <system_error>
#endif
LOG_CHANNEL(cubeb_dev_enum);
cubeb_enumerator::cubeb_enumerator() : audio_device_enumerator()
{
#ifdef _WIN32
// Cubeb requires COM to be initialized on the thread calling cubeb_init on Windows
HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
if (SUCCEEDED(hr))
{
com_init_success = true;
}
#endif
if (int err = cubeb_init(&ctx, "RPCS3 device enumeration", nullptr))
{
cubeb_dev_enum.error("cubeb_init() failed: %i", err);
ctx = nullptr;
}
}
cubeb_enumerator::~cubeb_enumerator()
{
if (ctx)
{
cubeb_destroy(ctx);
}
#ifdef _WIN32
if (com_init_success)
{
CoUninitialize();
}
#endif
}
std::vector<audio_device_enumerator::audio_device> cubeb_enumerator::get_output_devices()
{
if (ctx == nullptr)
{
return {};
}
cubeb_device_collection dev_collection{};
if (int err = cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &dev_collection))
{
cubeb_dev_enum.error("cubeb_enumerate_devices() failed: %i", err);
return {};
}
if (dev_collection.count == 0)
{
cubeb_dev_enum.error("No output devices available");
if (int err = cubeb_device_collection_destroy(ctx, &dev_collection))
{
cubeb_dev_enum.error("cubeb_device_collection_destroy() failed: %i", err);
}
return {};
}
std::vector<audio_device> device_list{};
for (u64 dev_idx = 0; dev_idx < dev_collection.count; dev_idx++)
{
const cubeb_device_info& dev_info = dev_collection.device[dev_idx];
if (dev_info.state == CUBEB_DEVICE_STATE_UNPLUGGED)
{
continue;
}
audio_device dev =
{
.id = std::string{dev_info.device_id},
.name = std::string{dev_info.friendly_name},
.max_ch = dev_info.max_channels
};
if (dev.id.empty())
{
cubeb_dev_enum.warning("Empty device id - skipping");
continue;
}
if (dev.name.empty())
{
dev.name = dev.id;
}
cubeb_dev_enum.notice("Found device: id=%s, name=%s, max_ch=%d", dev.id, dev.name, dev.max_ch);
device_list.emplace_back(dev);
}
if (int err = cubeb_device_collection_destroy(ctx, &dev_collection))
{
cubeb_dev_enum.error("cubeb_device_collection_destroy() failed: %i", err);
}
std::sort(device_list.begin(), device_list.end(), [](audio_device_enumerator::audio_device a, audio_device_enumerator::audio_device b)
{
return a.name < b.name;
});
return device_list;
}
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include "Emu/Audio/audio_device_enumerator.h"
#include "cubeb/cubeb.h"
class cubeb_enumerator final : public audio_device_enumerator
{
public:
cubeb_enumerator();
~cubeb_enumerator() override;
std::vector<audio_device> get_output_devices() override;
private:
cubeb* ctx{};
#ifdef _WIN32
bool com_init_success = false;
#endif
};
+45 -30
View File
@@ -6,6 +6,8 @@
#include "FAudioBackend.h"
#include "Emu/system_config.h"
#include "Emu/System.h"
#include "Emu/Audio/audio_device_enumerator.h"
#include "Utilities/StrUtil.h"
LOG_CHANNEL(FAudio_, "FAudio");
@@ -117,11 +119,10 @@ bool FAudioBackend::Initialized()
bool FAudioBackend::Operational()
{
std::lock_guard lock(m_error_cb_mutex);
return m_source_voice != nullptr && !m_reset_req;
return m_source_voice != nullptr && !m_reset_req.observe();
}
bool FAudioBackend::Open(AudioFreq freq, AudioSampleSize sample_size, AudioChannelCnt ch_cnt)
bool FAudioBackend::Open(std::string_view dev_id, AudioFreq freq, AudioSampleSize sample_size, AudioChannelCnt ch_cnt)
{
if (!Initialized())
{
@@ -132,9 +133,37 @@ bool FAudioBackend::Open(AudioFreq freq, AudioSampleSize sample_size, AudioChann
std::lock_guard lock(m_cb_mutex);
CloseUnlocked();
const bool use_default_dev = dev_id.empty() || dev_id == audio_device_enumerator::DEFAULT_DEV_ID;
u64 devid{};
if (!use_default_dev)
{
if (!try_to_uint64(&devid, dev_id, 0, UINT32_MAX))
{
FAudio_.error("Invalid device id - %s", dev_id);
return false;
}
}
if (u32 res = FAudio_CreateMasteringVoice(m_instance, &m_master_voice, FAUDIO_DEFAULT_CHANNELS, FAUDIO_DEFAULT_SAMPLERATE, 0, static_cast<u32>(devid), nullptr))
{
FAudio_.error("FAudio_CreateMasteringVoice() failed(0x%08x)", res);
m_master_voice = nullptr;
return false;
}
FAudioVoiceDetails vd{};
FAudioVoice_GetVoiceDetails(m_master_voice, &vd);
if (vd.InputChannels == 0)
{
FAudio_.error("Channel count of 0 is invalid");
CloseUnlocked();
return false;
}
m_sampling_rate = freq;
m_sample_size = sample_size;
m_channels = ch_cnt;
m_channels = static_cast<AudioChannelCnt>(std::min(static_cast<u32>(convert_channel_count(vd.InputChannels)), static_cast<u32>(ch_cnt)));;
FAudioWaveFormatEx waveformatex;
waveformatex.wFormatTag = get_convert_to_s16() ? FAUDIO_FORMAT_PCM : FAUDIO_FORMAT_IEEE_FLOAT;
@@ -153,43 +182,30 @@ bool FAudioBackend::Open(AudioFreq freq, AudioSampleSize sample_size, AudioChann
OnLoopEnd = nullptr;
OnVoiceError = nullptr;
if (u32 res = FAudio_CreateMasteringVoice(m_instance, &m_master_voice, FAUDIO_DEFAULT_CHANNELS, FAUDIO_DEFAULT_SAMPLERATE, 0, 0, nullptr))
{
FAudio_.error("FAudio_CreateMasteringVoice() failed(0x%08x)", res);
m_master_voice = nullptr;
}
else if (u32 res = FAudio_CreateSourceVoice(m_instance, &m_source_voice, &waveformatex, 0, FAUDIO_DEFAULT_FREQ_RATIO, this, nullptr, nullptr))
if (u32 res = FAudio_CreateSourceVoice(m_instance, &m_source_voice, &waveformatex, 0, FAUDIO_DEFAULT_FREQ_RATIO, this, nullptr, nullptr))
{
FAudio_.error("FAudio_CreateSourceVoice() failed(0x%08x)", res);
CloseUnlocked();
return false;
}
else if (u32 res = FAudioSourceVoice_Start(m_source_voice, 0, FAUDIO_COMMIT_NOW))
if (u32 res = FAudioSourceVoice_Start(m_source_voice, 0, FAUDIO_COMMIT_NOW))
{
FAudio_.error("FAudioSourceVoice_Start() failed(0x%08x)", res);
CloseUnlocked();
return false;
}
else if (u32 res = FAudioVoice_SetVolume(m_source_voice, 1.0f, FAUDIO_COMMIT_NOW))
if (u32 res = FAudioVoice_SetVolume(m_source_voice, 1.0f, FAUDIO_COMMIT_NOW))
{
FAudio_.error("FAudioVoice_SetVolume() failed(0x%08x)", res);
}
if (m_source_voice == nullptr)
{
FAudio_.error("Failed to open audio backend. Make sure that no other application is running that might block audio access (e.g. Netflix).");
return false;
}
m_data_buf.resize(get_sampling_rate() * get_sample_size() * get_channels() * INTERNAL_BUF_SIZE_MS * static_cast<u32>(FAUDIO_DEFAULT_FREQ_RATIO) / 1000);
m_data_buf.resize(get_sampling_rate() * get_sample_size() * get_channels() * INTERNAL_BUF_SIZE_MS / 1000);
return true;
}
void FAudioBackend::SetWriteCallback(std::function<u32(u32, void *)> cb)
{
std::lock_guard lock(m_cb_mutex);
m_write_callback = cb;
}
f64 FAudioBackend::GetCallbackFrameLen()
{
constexpr f64 _10ms = 0.01;
@@ -218,7 +234,7 @@ void FAudioBackend::OnVoiceProcessingPassStart_func(FAudioVoiceCallback *cb_obj,
FAudioBackend *faudio = static_cast<FAudioBackend *>(cb_obj);
std::unique_lock lock(faudio->m_cb_mutex, std::defer_lock);
if (BytesRequired && lock.try_lock() && faudio->m_write_callback && faudio->m_playing)
if (BytesRequired && !faudio->m_reset_req.observe() && lock.try_lock() && faudio->m_write_callback && faudio->m_playing)
{
ensure(BytesRequired <= faudio->m_data_buf.size(), "FAudio internal buffer is too small. Report to developers!");
@@ -250,11 +266,10 @@ void FAudioBackend::OnCriticalError_func(FAudioEngineCallback *cb_obj, u32 Error
FAudioBackend *faudio = static_cast<FAudioBackend *>(cb_obj);
std::lock_guard lock(faudio->m_error_cb_mutex);
faudio->m_reset_req = true;
std::lock_guard lock(faudio->m_state_cb_mutex);
if (faudio->m_error_callback)
if (!faudio->m_reset_req.test_and_set() && faudio->m_state_callback)
{
faudio->m_error_callback();
faudio->m_state_callback(AudioStateEvent::UNSPECIFIED_ERROR);
}
}
+2 -5
View File
@@ -24,10 +24,9 @@ public:
bool Initialized() override;
bool Operational() override;
bool Open(AudioFreq freq, AudioSampleSize sample_size, AudioChannelCnt ch_cnt) override;
bool Open(std::string_view dev_id, AudioFreq freq, AudioSampleSize sample_size, AudioChannelCnt ch_cnt) override;
void Close() override;
void SetWriteCallback(std::function<u32(u32, void *)> cb) override;
f64 GetCallbackFrameLen() override;
void Play() override;
@@ -40,12 +39,10 @@ private:
FAudioMasteringVoice* m_master_voice{};
FAudioSourceVoice* m_source_voice{};
shared_mutex m_cb_mutex{};
std::function<u32(u32, void *)> m_write_callback{};
std::vector<u8> m_data_buf{};
std::array<u8, sizeof(float) * static_cast<u32>(AudioChannelCnt::SURROUND_7_1)> m_last_sample{};
bool m_reset_req = false;
atomic_t<bool> m_reset_req = false;
// FAudio voice callbacks
static void OnVoiceProcessingPassStart_func(FAudioVoiceCallback *cb_obj, u32 BytesRequired);
@@ -0,0 +1,90 @@
#ifndef HAVE_FAUDIO
#error "FAudio support disabled but still being built."
#endif
#include "Emu/Audio/FAudio/faudio_enumerator.h"
#include <array>
#include <algorithm>
#include "Utilities/StrUtil.h"
#include "util/logs.hpp"
LOG_CHANNEL(faudio_dev_enum);
faudio_enumerator::faudio_enumerator() : audio_device_enumerator()
{
FAudio *tmp{};
if (u32 res = FAudioCreate(&tmp, 0, FAUDIO_DEFAULT_PROCESSOR))
{
faudio_dev_enum.error("FAudioCreate() failed(0x%08x)", res);
return;
}
// All succeeded, "commit"
instance = tmp;
}
faudio_enumerator::~faudio_enumerator()
{
if (instance != nullptr)
{
FAudio_StopEngine(instance);
FAudio_Release(instance);
}
}
std::vector<audio_device_enumerator::audio_device> faudio_enumerator::get_output_devices()
{
if (!instance)
{
return {};
}
u32 dev_cnt{};
if (u32 res = FAudio_GetDeviceCount(instance, &dev_cnt))
{
faudio_dev_enum.error("FAudio_GetDeviceCount() failed(0x%08x)", res);
return {};
}
if (dev_cnt == 0)
{
faudio_dev_enum.warning("No devices available");
return {};
}
std::vector<audio_device> device_list{};
for (u32 dev_idx = 0; dev_idx < dev_cnt; dev_idx++)
{
FAudioDeviceDetails dev_info{};
if (u32 res = FAudio_GetDeviceDetails(instance, dev_idx, &dev_info))
{
faudio_dev_enum.error("FAudio_GetDeviceDetails() failed(0x%08x)", res);
continue;
}
audio_device dev =
{
.id = std::to_string(dev_idx),
.name = utf16_to_utf8(std::bit_cast<char16_t*>(&dev_info.DisplayName[0])),
.max_ch = dev_info.OutputFormat.Format.nChannels
};
if (dev.name.empty())
{
dev.name = "Device " + dev.id;
}
faudio_dev_enum.notice("Found device: id=%s, name=%s, max_ch=%d", dev.id, dev.name, dev.max_ch);
device_list.emplace_back(dev);
}
std::sort(device_list.begin(), device_list.end(), [](audio_device_enumerator::audio_device a, audio_device_enumerator::audio_device b)
{
return a.name < b.name;
});
return device_list;
}
@@ -0,0 +1,22 @@
#pragma once
#ifndef HAVE_FAUDIO
#error "FAudio support disabled but still being built."
#endif
#include "Emu/Audio/audio_device_enumerator.h"
#include "FAudio.h"
class faudio_enumerator final : public audio_device_enumerator
{
public:
faudio_enumerator();
~faudio_enumerator() override;
std::vector<audio_device> get_output_devices() override;
private:
FAudio* instance{};
};
+1 -4
View File
@@ -10,18 +10,15 @@ public:
std::string_view GetName() const override { return "Null"sv; }
bool Open(AudioFreq /* freq */, AudioSampleSize /* sample_size */, AudioChannelCnt /* ch_cnt */) override
bool Open(std::string_view /* dev_id */, AudioFreq /* freq */, AudioSampleSize /* sample_size */, AudioChannelCnt /* ch_cnt */) override
{
Close();
return true;
}
void Close() override { m_playing = false; }
void SetWriteCallback(std::function<u32(u32, void *)> /* cb */) override {};
f64 GetCallbackFrameLen() override { return 0.01; };
void SetErrorCallback(std::function<void()> /* cb */) override {};
void Play() override { m_playing = true; }
void Pause() override { m_playing = false; }
bool IsPlaying() override { return m_playing; }
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include "Emu/Audio/audio_device_enumerator.h"
class null_enumerator final : public audio_device_enumerator
{
public:
null_enumerator() {};
~null_enumerator() override {};
std::vector<audio_device> get_output_devices() override { return {}; }
};
+179 -46
View File
@@ -5,6 +5,8 @@
#include <algorithm>
#include "util/logs.hpp"
#include "Emu/System.h"
#include "Emu/Audio/audio_device_enumerator.h"
#include "Utilities/StrUtil.h"
#include "XAudio2Backend.h"
#include <Windows.h>
@@ -14,42 +16,91 @@
LOG_CHANNEL(XAudio);
template <>
void fmt_class_string<ERole>::format(std::string& out, u64 arg)
{
format_enum(out, arg, [](auto value)
{
switch (value)
{
case eConsole: return "eConsole";
case eMultimedia: return "eMultimedia";
case eCommunications: return "eCommunications";
}
return unknown;
});
}
template <>
void fmt_class_string<EDataFlow>::format(std::string& out, u64 arg)
{
format_enum(out, arg, [](auto value)
{
switch (value)
{
case eRender: return "eRender";
case eCapture: return "eCapture";
case eAll: return "eAll";
}
return unknown;
});
}
XAudio2Backend::XAudio2Backend()
: AudioBackend()
{
Microsoft::WRL::ComPtr<IXAudio2> instance;
Microsoft::WRL::ComPtr<IXAudio2> instance{};
Microsoft::WRL::ComPtr<IMMDeviceEnumerator> enumerator{};
// In order to prevent errors on CreateMasteringVoice, apparently we need CoInitializeEx according to:
// https://docs.microsoft.com/en-us/windows/win32/api/xaudio2fx/nf-xaudio2fx-xaudio2createvolumemeter
HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
if (SUCCEEDED(hr))
if (HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); SUCCEEDED(hr))
{
m_com_init_success = true;
}
hr = XAudio2Create(instance.GetAddressOf(), 0, XAUDIO2_USE_DEFAULT_PROCESSOR);
if (FAILED(hr))
if (HRESULT hr = XAudio2Create(instance.GetAddressOf(), 0, XAUDIO2_USE_DEFAULT_PROCESSOR); FAILED(hr))
{
XAudio.error("XAudio2Create() failed: %s (0x%08x)", std::system_category().message(hr), static_cast<u32>(hr));
return;
}
hr = instance->RegisterForCallbacks(this);
if (FAILED(hr))
if (HRESULT hr = instance->RegisterForCallbacks(this); FAILED(hr))
{
// Some error recovery functionality will be lost, but otherwise backend is operational
XAudio.error("RegisterForCallbacks() failed: %s (0x%08x)", std::system_category().message(hr), static_cast<u32>(hr));
}
// Try to register a listener for device changes
if (HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(enumerator.GetAddressOf())); FAILED(hr))
{
XAudio.error("CoCreateInstance() failed: %s (0x%08x)", std::system_category().message(hr), static_cast<u32>(hr));
return;
}
if (HRESULT hr = enumerator->RegisterEndpointNotificationCallback(this); FAILED(hr))
{
XAudio.error("RegisterEndpointNotificationCallback() failed: %s (0x%08x)", std::system_category().message(hr), static_cast<u32>(hr));
return;
}
// All succeeded, "commit"
m_xaudio2_instance = std::move(instance);
m_device_enumerator = std::move(enumerator);
}
XAudio2Backend::~XAudio2Backend()
{
Close();
if (m_device_enumerator != nullptr)
{
m_device_enumerator->UnregisterEndpointNotificationCallback(this);
m_device_enumerator = nullptr;
}
if (m_xaudio2_instance != nullptr)
{
m_xaudio2_instance->StopEngine();
@@ -69,14 +120,13 @@ bool XAudio2Backend::Initialized()
bool XAudio2Backend::Operational()
{
std::lock_guard lock(m_error_cb_mutex);
return m_source_voice != nullptr && !m_reset_req.observe();
}
if (m_dev_listener.output_device_changed())
{
m_reset_req = true;
}
return m_source_voice != nullptr && !m_reset_req;
bool XAudio2Backend::DefaultDeviceChanged()
{
std::lock_guard lock{m_dev_sw_mutex};
return !m_reset_req.observe() && m_default_dev_changed;
}
void XAudio2Backend::Play()
@@ -114,11 +164,15 @@ void XAudio2Backend::CloseUnlocked()
m_playing = false;
m_last_sample.fill(0);
m_default_dev_changed = false;
m_current_device.clear();
}
void XAudio2Backend::Close()
{
std::lock_guard lock(m_cb_mutex);
std::lock_guard dev_sw_lock{m_dev_sw_mutex};
CloseUnlocked();
}
@@ -144,7 +198,7 @@ void XAudio2Backend::Pause()
}
}
bool XAudio2Backend::Open(AudioFreq freq, AudioSampleSize sample_size, AudioChannelCnt ch_cnt)
bool XAudio2Backend::Open(std::string_view dev_id, AudioFreq freq, AudioSampleSize sample_size, AudioChannelCnt ch_cnt)
{
if (!Initialized())
{
@@ -153,11 +207,57 @@ bool XAudio2Backend::Open(AudioFreq freq, AudioSampleSize sample_size, AudioChan
}
std::lock_guard lock(m_cb_mutex);
std::lock_guard dev_sw_lock{m_dev_sw_mutex};
CloseUnlocked();
const bool use_default_device = dev_id.empty() || dev_id == audio_device_enumerator::DEFAULT_DEV_ID;
std::string selected_dev_id{};
if (use_default_device)
{
Microsoft::WRL::ComPtr<IMMDevice> default_dev{};
if (HRESULT hr = m_device_enumerator->GetDefaultAudioEndpoint(eRender, eConsole, default_dev.GetAddressOf()); FAILED(hr))
{
XAudio.error("GetDefaultAudioEndpoint() failed: %s (0x%08x)", std::system_category().message(hr), static_cast<u32>(hr));
return false;
}
LPWSTR default_id{};
if (HRESULT hr = default_dev->GetId(&default_id); FAILED(hr))
{
XAudio.error("GetId() failed: %s (0x%08x)", std::system_category().message(hr), static_cast<u32>(hr));
return false;
}
selected_dev_id = wchar_to_utf8(std::wstring_view{default_id});
CoTaskMemFree(default_id);
if (selected_dev_id.empty())
{
XAudio.error("Default device id is empty");
return false;
}
}
if (HRESULT hr = m_xaudio2_instance->CreateMasteringVoice(&m_master_voice, 0, 0, 0, utf8_to_wchar(use_default_device ? selected_dev_id : dev_id).c_str()); FAILED(hr))
{
XAudio.error("CreateMasteringVoice() failed: %s (0x%08x)", std::system_category().message(hr), static_cast<u32>(hr));
m_master_voice = nullptr;
return false;
}
XAUDIO2_VOICE_DETAILS vd{};
m_master_voice->GetVoiceDetails(&vd);
if (vd.InputChannels == 0)
{
XAudio.error("Channel count 0 is invalid");
CloseUnlocked();
return false;
}
m_sampling_rate = freq;
m_sample_size = sample_size;
m_channels = ch_cnt;
m_channels = static_cast<AudioChannelCnt>(std::min(static_cast<u32>(convert_channel_count(vd.InputChannels)), static_cast<u32>(ch_cnt)));
WAVEFORMATEX waveformatex{};
waveformatex.wFormatTag = get_convert_to_s16() ? WAVE_FORMAT_PCM : WAVE_FORMAT_IEEE_FLOAT;
@@ -168,65 +268,56 @@ bool XAudio2Backend::Open(AudioFreq freq, AudioSampleSize sample_size, AudioChan
waveformatex.wBitsPerSample = get_sample_size() * 8;
waveformatex.cbSize = 0;
if (HRESULT hr = m_xaudio2_instance->CreateMasteringVoice(&m_master_voice); FAILED(hr))
{
XAudio.error("CreateMasteringVoice() failed: %s (0x%08x)", std::system_category().message(hr), static_cast<u32>(hr));
m_master_voice = nullptr;
}
else if (HRESULT hr = m_xaudio2_instance->CreateSourceVoice(&m_source_voice, &waveformatex, 0, XAUDIO2_DEFAULT_FREQ_RATIO, this); FAILED(hr))
if (HRESULT hr = m_xaudio2_instance->CreateSourceVoice(&m_source_voice, &waveformatex, 0, XAUDIO2_DEFAULT_FREQ_RATIO, this); FAILED(hr))
{
XAudio.error("CreateSourceVoice() failed: %s (0x%08x)", std::system_category().message(hr), static_cast<u32>(hr));
CloseUnlocked();
return false;
}
else if (HRESULT hr = m_source_voice->Start(); FAILED(hr))
if (HRESULT hr = m_source_voice->Start(); FAILED(hr))
{
XAudio.error("Start() failed: %s (0x%08x)", std::system_category().message(hr), static_cast<u32>(hr));
CloseUnlocked();
return false;
}
else if (HRESULT hr = m_source_voice->SetVolume(1.0f); FAILED(hr))
if (HRESULT hr = m_source_voice->SetVolume(1.0f); FAILED(hr))
{
XAudio.error("SetVolume() failed: %s (0x%08x)", std::system_category().message(hr), static_cast<u32>(hr));
}
if (m_source_voice == nullptr)
m_data_buf.resize(get_sampling_rate() * get_sample_size() * get_channels() * INTERNAL_BUF_SIZE_MS / 1000);
if (use_default_device)
{
XAudio.error("Failed to open audio backend. Make sure that no other application is running that might block audio access (e.g. Netflix).");
return false;
m_current_device = selected_dev_id;
}
m_data_buf.resize(get_sampling_rate() * get_sample_size() * get_channels() * INTERNAL_BUF_SIZE_MS * static_cast<u32>(XAUDIO2_DEFAULT_FREQ_RATIO) / 1000);
return true;
}
void XAudio2Backend::SetWriteCallback(std::function<u32(u32, void *)> cb)
{
std::lock_guard lock(m_cb_mutex);
m_write_callback = cb;
}
f64 XAudio2Backend::GetCallbackFrameLen()
{
constexpr f64 _10ms = 0.01;
if (m_source_voice == nullptr)
if (m_xaudio2_instance == nullptr)
{
XAudio.error("GetCallbackFrameLen() called uninitialized");
return _10ms;
}
void *ext;
Microsoft::WRL::ComPtr<IXAudio2Extension> xaudio_ext{};
f64 min_latency{};
const HRESULT hr = m_xaudio2_instance->QueryInterface(IID_IXAudio2Extension, &ext);
if (FAILED(hr))
if (HRESULT hr = m_xaudio2_instance->QueryInterface(IID_IXAudio2Extension, std::bit_cast<void**>(xaudio_ext.GetAddressOf())); FAILED(hr))
{
XAudio.error("QueryInterface() failed: %s (0x%08x)", std::system_category().message(hr), static_cast<u32>(hr));
}
else
{
u32 samples_per_q = 0, freq = 0;
static_cast<IXAudio2Extension *>(ext)->GetProcessingQuantum(&samples_per_q, &freq);
xaudio_ext->GetProcessingQuantum(&samples_per_q, &freq);
if (freq)
{
@@ -240,7 +331,7 @@ f64 XAudio2Backend::GetCallbackFrameLen()
void XAudio2Backend::OnVoiceProcessingPassStart(UINT32 BytesRequired)
{
std::unique_lock lock(m_cb_mutex, std::defer_lock);
if (BytesRequired && lock.try_lock() && m_write_callback && m_playing)
if (BytesRequired && !m_reset_req.observe() && lock.try_lock() && m_write_callback && m_playing)
{
ensure(BytesRequired <= m_data_buf.size(), "XAudio internal buffer is too small. Report to developers!");
@@ -270,11 +361,53 @@ void XAudio2Backend::OnCriticalError(HRESULT Error)
{
XAudio.error("OnCriticalError() called: %s (0x%08x)", std::system_category().message(Error), static_cast<u32>(Error));
std::lock_guard lock(m_error_cb_mutex);
m_reset_req = true;
std::lock_guard lock(m_state_cb_mutex);
if (m_error_callback)
if (!m_reset_req.test_and_set() && m_state_callback)
{
m_error_callback();
m_state_callback(AudioStateEvent::UNSPECIFIED_ERROR);
}
}
HRESULT XAudio2Backend::OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR new_default_device_id)
{
XAudio.notice("OnDefaultDeviceChanged(flow=%s, role=%s, new_default_device_id=0x%x)", flow, role, new_default_device_id);
if (!new_default_device_id)
{
XAudio.notice("OnDefaultDeviceChanged(): new_default_device_id empty");
return S_OK;
}
// Listen only for one device role, otherwise we're going to receive more than one notification for flow type
if (role != eConsole)
{
XAudio.notice("OnDefaultDeviceChanged(): we don't care about this device");
return S_OK;
}
std::lock_guard lock{m_dev_sw_mutex};
// Non default device is used
if (m_current_device.empty())
{
return S_OK;
}
const std::string new_device_id = wchar_to_utf8(std::wstring_view{new_default_device_id});
if (flow == eRender || flow == eAll)
{
if (!m_reset_req.observe() && new_device_id != m_current_device)
{
m_default_dev_changed = true;
if (m_state_callback)
{
m_state_callback(AudioStateEvent::DEFAULT_DEVICE_CHANGED);
}
}
}
return S_OK;
}
+21 -9
View File
@@ -7,12 +7,12 @@
#include <memory>
#include "Utilities/mutex.h"
#include "Emu/Audio/AudioBackend.h"
#include "Emu/Audio/audio_device_listener.h"
#include <xaudio2redist.h>
#include <wrl/client.h>
#include <MMDeviceAPI.h>
class XAudio2Backend final : public AudioBackend, public IXAudio2VoiceCallback, public IXAudio2EngineCallback
class XAudio2Backend final : public AudioBackend, public IXAudio2VoiceCallback, public IXAudio2EngineCallback, public IMMNotificationClient
{
public:
XAudio2Backend();
@@ -25,11 +25,11 @@ public:
bool Initialized() override;
bool Operational() override;
bool DefaultDeviceChanged() override;
bool Open(AudioFreq freq, AudioSampleSize sample_size, AudioChannelCnt ch_cnt) override;
bool Open(std::string_view dev_id, AudioFreq freq, AudioSampleSize sample_size, AudioChannelCnt ch_cnt) override;
void Close() override;
void SetWriteCallback(std::function<u32(u32, void *)> cb) override;
f64 GetCallbackFrameLen() override;
void Play() override;
@@ -43,14 +43,16 @@ private:
IXAudio2SourceVoice* m_source_voice{};
bool m_com_init_success = false;
shared_mutex m_cb_mutex{};
std::function<u32(u32, void *)> m_write_callback{};
Microsoft::WRL::ComPtr<IMMDeviceEnumerator> m_device_enumerator{};
shared_mutex m_dev_sw_mutex{};
std::string m_current_device{};
bool m_default_dev_changed = false;
std::vector<u8> m_data_buf{};
std::array<u8, sizeof(float) * static_cast<u32>(AudioChannelCnt::SURROUND_7_1)> m_last_sample{};
bool m_reset_req = false;
audio_device_listener m_dev_listener{};
atomic_t<bool> m_reset_req = false;
// XAudio voice callbacks
void OnVoiceProcessingPassStart(UINT32 BytesRequired) override;
@@ -66,5 +68,15 @@ private:
void OnProcessingPassEnd() override {};
void OnCriticalError(HRESULT Error) override;
// IMMNotificationClient callbacks
IFACEMETHODIMP_(ULONG) AddRef() override { return 1; };
IFACEMETHODIMP_(ULONG) Release() override { return 1; };
IFACEMETHODIMP QueryInterface(REFIID /*iid*/, void** /*object*/) override { return E_NOINTERFACE; };
IFACEMETHODIMP OnPropertyValueChanged(LPCWSTR /*device_id*/, const PROPERTYKEY /*key*/) override { return S_OK; };
IFACEMETHODIMP OnDeviceAdded(LPCWSTR /*device_id*/) override { return S_OK; };
IFACEMETHODIMP OnDeviceRemoved(LPCWSTR /*device_id*/) override { return S_OK; };
IFACEMETHODIMP OnDeviceStateChanged(LPCWSTR /*device_id*/, DWORD /*new_state*/) override { return S_OK; };
IFACEMETHODIMP OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR new_default_device_id) override;
void CloseUnlocked();
};
@@ -0,0 +1,153 @@
#ifndef _WIN32
#error "XAudio2 can only be built on Windows."
#endif
#include "Emu/Audio/XAudio2/xaudio2_enumerator.h"
#include "util/logs.hpp"
#include "Utilities/StrUtil.h"
#include <algorithm>
#include <wrl/client.h>
#include <Windows.h>
#include <system_error>
#include <mmdeviceapi.h>
#include <Functiondiscoverykeys_devpkey.h>
LOG_CHANNEL(xaudio_dev_enum);
xaudio2_enumerator::xaudio2_enumerator() : audio_device_enumerator()
{
}
xaudio2_enumerator::~xaudio2_enumerator()
{
}
std::vector<audio_device_enumerator::audio_device> xaudio2_enumerator::get_output_devices()
{
Microsoft::WRL::ComPtr<IMMDeviceEnumerator> devEnum{};
if (HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(devEnum.GetAddressOf())); FAILED(hr))
{
xaudio_dev_enum.error("CoCreateInstance() failed: %s (0x%08x)", std::system_category().message(hr), static_cast<u32>(hr));
return {};
}
Microsoft::WRL::ComPtr<IMMDeviceCollection> devices{};
if (HRESULT hr = devEnum->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE | DEVICE_STATE_DISABLED, &devices); FAILED(hr))
{
xaudio_dev_enum.error("EnumAudioEndpoints() failed: %s (0x%08x)", std::system_category().message(hr), static_cast<u32>(hr));
return {};
}
UINT count = 0;
if (HRESULT hr = devices->GetCount(&count); FAILED(hr))
{
xaudio_dev_enum.error("devices->GetCount() failed: %s (0x%08x)", std::system_category().message(hr), static_cast<u32>(hr));
return {};
}
if (count == 0)
{
xaudio_dev_enum.warning("No devices available");
return {};
}
std::vector<audio_device> device_list{};
for (UINT dev_idx = 0; dev_idx < count; dev_idx++)
{
Microsoft::WRL::ComPtr<IMMDevice> endpoint{};
if (HRESULT hr = devices->Item(dev_idx, endpoint.GetAddressOf()); FAILED(hr))
{
xaudio_dev_enum.error("devices->Item() failed: %s (0x%08x)", std::system_category().message(hr), static_cast<u32>(hr));
continue;
}
LPWSTR id = nullptr;
if (HRESULT hr = endpoint->GetId(&id); FAILED(hr))
{
xaudio_dev_enum.error("endpoint->GetId() failed: %s (0x%08x)", std::system_category().message(hr), static_cast<u32>(hr));
continue;
}
if (std::wstring_view{id}.empty())
{
xaudio_dev_enum.error("Empty device id - skipping");
CoTaskMemFree(id);
continue;
}
audio_device dev{};
dev.id = wchar_to_utf8(id);
CoTaskMemFree(id);
Microsoft::WRL::ComPtr<IPropertyStore> props{};
if (HRESULT hr = endpoint->OpenPropertyStore(STGM_READ, props.GetAddressOf()); FAILED(hr))
{
xaudio_dev_enum.error("endpoint->OpenPropertyStore() failed: %s (0x%08x)", std::system_category().message(hr), static_cast<u32>(hr));
continue;
}
PROPVARIANT var;
PropVariantInit(&var);
if (HRESULT hr = props->GetValue(PKEY_Device_FriendlyName, &var); FAILED(hr))
{
xaudio_dev_enum.error("props->GetValue() failed: %s (0x%08x)", std::system_category().message(hr), static_cast<u32>(hr));
PropVariantClear(&var);
continue;
}
if (var.vt != VT_LPWSTR)
{
PropVariantClear(&var);
continue;
}
dev.name = wchar_to_utf8(var.pwszVal);
if (dev.name.empty())
{
dev.name = dev.id;
}
PropVariantClear(&var);
dev.max_ch = 2;
if (HRESULT hr = props->GetValue(PKEY_AudioEngine_DeviceFormat, &var); SUCCEEDED(hr))
{
if (var.vt == VT_BLOB)
{
if (var.blob.cbSize == sizeof(PCMWAVEFORMAT))
{
const PCMWAVEFORMAT* pcm = std::bit_cast<const PCMWAVEFORMAT*>(var.blob.pBlobData);
dev.max_ch = pcm->wf.nChannels;
}
else if (var.blob.cbSize >= sizeof(WAVEFORMATEX))
{
const WAVEFORMATEX* wfx = std::bit_cast<const WAVEFORMATEX*>(var.blob.pBlobData);
if (var.blob.cbSize >= sizeof(WAVEFORMATEX) + wfx->cbSize || wfx->wFormatTag == WAVE_FORMAT_PCM)
{
dev.max_ch = wfx->nChannels;
}
}
}
}
else
{
xaudio_dev_enum.error("props->GetValue() failed: %s (0x%08x)", std::system_category().message(hr), static_cast<u32>(hr));
}
PropVariantClear(&var);
xaudio_dev_enum.notice("Found device: id=%s, name=%s, max_ch=%d", dev.id, dev.name, dev.max_ch);
device_list.emplace_back(dev);
}
std::sort(device_list.begin(), device_list.end(), [](audio_device_enumerator::audio_device a, audio_device_enumerator::audio_device b)
{
return a.name < b.name;
});
return device_list;
}
@@ -0,0 +1,17 @@
#pragma once
#ifndef _WIN32
#error "XAudio2 can only be built on Windows."
#endif
#include "Emu/Audio/audio_device_enumerator.h"
class xaudio2_enumerator final : public audio_device_enumerator
{
public:
xaudio2_enumerator();
~xaudio2_enumerator() override;
std::vector<audio_device> get_output_devices() override;
};

Some files were not shown because too many files have changed in this diff Show More