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

This commit is contained in:
Vestrel 2022-07-09 00:13:38 +09:00 committed by GitHub
parent 4b787b22c8
commit 98b730c806
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1235 additions and 395 deletions

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

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>

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>

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>

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");
}
}

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;

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);
}
}
}

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);
};

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;
}

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
};

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);
}
}

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);

View File

@ -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;
}

View File

@ -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{};
};

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; }

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 {}; }
};

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;
}

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();
};

View File

@ -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;
}

View File

@ -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;
};

View File

@ -0,0 +1,26 @@
#pragma once
#include "util/types.hpp"
#include <vector>
#include <string>
class audio_device_enumerator
{
public:
static constexpr std::string_view DEFAULT_DEV_ID = "@@@default@@@";
struct audio_device
{
std::string id{};
std::string name{};
usz max_ch{};
};
audio_device_enumerator() {};
virtual ~audio_device_enumerator() = default;
// Enumerate available output devices.
virtual std::vector<audio_device> get_output_devices() = 0;
};

View File

@ -1,147 +0,0 @@
#include "stdafx.h"
#include "audio_device_listener.h"
#include "util/logs.hpp"
#include "Utilities/StrUtil.h"
#include "Emu/Cell/Modules/cellAudio.h"
#include "Emu/IdManager.h"
LOG_CHANNEL(IO);
audio_device_listener::audio_device_listener()
{
#ifdef _WIN32
HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
if (SUCCEEDED(hr))
{
m_com_init_success = true;
}
// Try to register a listener for device changes
hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&m_device_enumerator));
if (hr != S_OK)
{
IO.error("CoCreateInstance() failed: %s (0x%08x)", std::system_category().message(hr), static_cast<u32>(hr));
}
else if (m_device_enumerator)
{
hr = m_device_enumerator->RegisterEndpointNotificationCallback(this);
if (FAILED(hr))
{
IO.error("RegisterEndpointNotificationCallback() failed: %s (0x%08x)", std::system_category().message(hr), static_cast<u32>(hr));
m_device_enumerator->Release();
}
}
else
{
IO.error("Device enumerator invalid");
}
#endif
}
audio_device_listener::~audio_device_listener()
{
#ifdef _WIN32
if (m_device_enumerator != nullptr)
{
m_device_enumerator->UnregisterEndpointNotificationCallback(this);
m_device_enumerator->Release();
}
if (m_com_init_success)
{
CoUninitialize();
}
#endif
}
bool audio_device_listener::input_device_changed()
{
return m_input_device_changed.test_and_reset();
}
bool audio_device_listener::output_device_changed()
{
return m_output_device_changed.test_and_reset();
}
#ifdef _WIN32
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;
});
}
HRESULT audio_device_listener::OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR new_default_device_id)
{
IO.notice("OnDefaultDeviceChanged(flow=%s, role=%s, new_default_device_id=0x%x)", flow, role, new_default_device_id);
if (!new_default_device_id)
{
IO.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)
{
IO.notice("OnDefaultDeviceChanged(): we don't care about this device");
return S_OK;
}
const std::wstring tmp(new_default_device_id);
const std::string new_device_id = wchar_to_utf8(tmp.c_str());
if (flow == eRender || flow == eAll)
{
if (output_device_id != new_device_id)
{
output_device_id = new_device_id;
IO.warning("Default output device changed: new device = '%s'", output_device_id);
m_output_device_changed = true;
}
}
if (flow == eCapture || flow == eAll)
{
if (input_device_id != new_device_id)
{
input_device_id = new_device_id;
IO.warning("Default input device changed: new device = '%s'", input_device_id);
m_input_device_changed = true;
}
}
return S_OK;
}
#endif

View File

@ -1,42 +0,0 @@
#pragma once
#ifdef _WIN32
#include <MMDeviceAPI.h>
#endif
#include "util/atomic.hpp"
#ifdef _WIN32
class audio_device_listener : public IMMNotificationClient
#else
class audio_device_listener
#endif
{
public:
audio_device_listener();
~audio_device_listener();
bool input_device_changed();
bool output_device_changed();
private:
#ifdef _WIN32
std::string input_device_id;
std::string output_device_id;
IFACEMETHODIMP_(ULONG) AddRef() override { return 1; };
IFACEMETHODIMP_(ULONG) Release() override { return 1; };
IFACEMETHODIMP QueryInterface(REFIID /*iid*/, void** /*object*/) override { return S_OK; };
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;
IMMDeviceEnumerator* m_device_enumerator = nullptr;
bool m_com_init_success = false;
#endif
atomic_t<bool> m_input_device_changed = false;
atomic_t<bool> m_output_device_changed = false;
};

View File

@ -118,17 +118,20 @@ target_sources(rpcs3_emu PRIVATE
# Audio
target_sources(rpcs3_emu PRIVATE
Audio/audio_device_listener.cpp
Audio/audio_resampler.cpp
Audio/AudioDumper.cpp
Audio/AudioBackend.cpp
Audio/Cubeb/CubebBackend.cpp
Audio/Cubeb/cubeb_enumerator.cpp
)
if(USE_FAUDIO)
find_package(SDL2)
if(SDL2_FOUND AND NOT SDL2_VERSION VERSION_LESS 2.0.9)
target_sources(rpcs3_emu PRIVATE Audio/FAudio/FAudioBackend.cpp)
target_sources(rpcs3_emu PRIVATE
Audio/FAudio/FAudioBackend.cpp
Audio/FAudio/faudio_enumerator.cpp
)
target_link_libraries(rpcs3_emu PUBLIC 3rdparty::faudio)
endif()
endif()
@ -139,6 +142,7 @@ if(WIN32)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /DELAYLOAD:xaudio2_9redist.dll")
target_sources(rpcs3_emu PRIVATE
Audio/XAudio2/XAudio2Backend.cpp
Audio/XAudio2/xaudio2_enumerator.cpp
)
endif()

View File

@ -66,11 +66,23 @@ void cell_audio_config::reset(bool backend_changed)
const AudioFreq freq = AudioFreq::FREQ_48K;
const AudioSampleSize sample_size = raw.convert_to_s16 ? AudioSampleSize::S16 : AudioSampleSize::FLOAT;
const auto [ch_cnt, downmix] = AudioBackend::get_channel_count_and_downmixer(0); // CELL_AUDIO_OUT_PRIMARY
const f64 cb_frame_len = backend->Open(freq, sample_size, ch_cnt) ? backend->GetCallbackFrameLen() : 0.0;
const auto [req_ch_cnt, downmix] = AudioBackend::get_channel_count_and_downmixer(0); // CELL_AUDIO_OUT_PRIMARY
f64 cb_frame_len = 0.0;
u32 ch_cnt = 2;
if (backend->Open(raw.audio_device, freq, sample_size, req_ch_cnt))
{
cb_frame_len = backend->GetCallbackFrameLen();
ch_cnt = backend->get_channels();
}
else
{
cellAudio.error("Failed to open audio backend. Make sure that no other application is running that might block audio access (e.g. Netflix).");
}
audio_downmix = downmix;
audio_channels = static_cast<u32>(ch_cnt);
audio_channels = ch_cnt;
audio_sampling_rate = static_cast<u32>(freq);
audio_block_period = AUDIO_BUFFER_SAMPLES * 1'000'000 / audio_sampling_rate;
audio_sample_size = static_cast<u32>(sample_size);
@ -567,6 +579,7 @@ namespace audio
{
return
{
.audio_device = g_cfg.audio.audio_device,
.buffering_enabled = static_cast<bool>(g_cfg.audio.enable_buffering),
.desired_buffer_duration = g_cfg.audio.desired_buffer_duration,
.enable_time_stretching = static_cast<bool>(g_cfg.audio.enable_time_stretching),
@ -592,6 +605,7 @@ namespace audio
if (const auto raw = g_audio.cfg.raw;
force_reset ||
raw.audio_device != new_raw.audio_device ||
raw.desired_buffer_duration != new_raw.desired_buffer_duration ||
raw.buffering_enabled != new_raw.buffering_enabled ||
raw.time_stretching_threshold != new_raw.time_stretching_threshold ||
@ -699,6 +713,13 @@ void cell_audio_thread::operator()()
continue;
}
if (ringbuffer->device_changed())
{
cellAudio.warning("Default device changed, attempting to switch...");
update_config(false);
continue;
}
if (m_backend_failed)
{
cellAudio.warning("Backend recovered");

View File

@ -205,6 +205,7 @@ struct cell_audio_config
{
struct raw_config
{
std::string audio_device{};
bool buffering_enabled = false;
s64 desired_buffer_duration = 0;
bool enable_time_stretching = false;
@ -343,6 +344,11 @@ public:
return backend->Operational();
}
bool device_changed() const
{
return backend->DefaultDeviceChanged();
}
std::string_view get_backend_name() const
{
return backend->GetName();

View File

@ -1294,7 +1294,7 @@ rsxaudio_backend_thread::~rsxaudio_backend_thread()
{
backend->Close();
backend->SetWriteCallback(nullptr);
backend->SetErrorCallback(nullptr);
backend->SetStateCallback(nullptr);
backend = nullptr;
}
}
@ -1327,6 +1327,7 @@ rsxaudio_backend_thread::emu_audio_cfg rsxaudio_backend_thread::get_emu_cfg()
emu_audio_cfg cfg =
{
.audio_device = g_cfg.audio.audio_device,
.desired_buffer_duration = g_cfg.audio.desired_buffer_duration,
.time_stretching_threshold = g_cfg.audio.time_stretching_threshold / 100.0,
.buffering_enabled = static_cast<bool>(g_cfg.audio.enable_buffering),
@ -1366,7 +1367,7 @@ void rsxaudio_backend_thread::operator()()
std::unique_lock lock(state_update_m);
for (;;)
{
// Unsafe to access backend under lock (error_callback uses state_update_m -> possible deadlock)
// Unsafe to access backend under lock (state_changed_callback uses state_update_m -> possible deadlock)
if (thread_ctrl::state() == thread_state::aborting)
{
@ -1407,6 +1408,13 @@ void rsxaudio_backend_thread::operator()()
reset_backend = true;
should_update_backend = true;
backend_error_occured = false;
backend_device_changed = false;
}
if (backend_device_changed)
{
should_update_backend = true;
backend_device_changed = false;
}
if (should_update_backend)
@ -1669,15 +1677,26 @@ void rsxaudio_backend_thread::backend_init(const rsxaudio_state& ra_state, const
backend = nullptr;
backend = Emu.GetCallbacks().get_audio();
backend->SetWriteCallback(std::bind(&rsxaudio_backend_thread::write_data_callback, this, std::placeholders::_1, std::placeholders::_2));
backend->SetErrorCallback(std::bind(&rsxaudio_backend_thread::error_callback, this));
backend->SetStateCallback(std::bind(&rsxaudio_backend_thread::state_changed_callback, this, std::placeholders::_1));
}
const port_config& port_cfg = ra_state.port[static_cast<u8>(emu_cfg.avport)];
const AudioSampleSize sample_size = emu_cfg.convert_to_s16 ? AudioSampleSize::S16 : AudioSampleSize::FLOAT;
const AudioChannelCnt ch_cnt = static_cast<AudioChannelCnt>(std::min<u32>(static_cast<u32>(port_cfg.ch_cnt), static_cast<u32>(emu_cfg.channels)));
f64 cb_frame_len = 0.0;
u32 backend_ch_cnt = 2;
if (backend->Open(emu_cfg.audio_device, port_cfg.freq, sample_size, ch_cnt))
{
cb_frame_len = backend->GetCallbackFrameLen();
backend_ch_cnt = backend->get_channels();
}
else
{
sys_rsxaudio.error("Failed to open audio backend. Make sure that no other application is running that might block audio access (e.g. Netflix).");
}
static constexpr f64 _10ms = 512.0 / 48000.0;
const f64 cb_frame_len = backend->Open(port_cfg.freq, sample_size, ch_cnt) ? backend->GetCallbackFrameLen() : 0.0;
const f64 buffering_len = emu_cfg.buffering_enabled ? (emu_cfg.desired_buffer_duration / 1000.0) : 0.0;
const u64 bytes_per_sec = static_cast<u32>(AudioSampleSize::FLOAT) * static_cast<u32>(port_cfg.ch_cnt) * static_cast<u32>(port_cfg.freq);
@ -1711,7 +1730,7 @@ void rsxaudio_backend_thread::backend_init(const rsxaudio_state& ra_state, const
{
val.freq = static_cast<u32>(port_cfg.freq);
val.input_ch_cnt = static_cast<u32>(port_cfg.ch_cnt);
val.output_ch_cnt = static_cast<u32>(ch_cnt);
val.output_ch_cnt = backend_ch_cnt;
val.convert_to_s16 = emu_cfg.convert_to_s16;
val.avport_idx = emu_cfg.avport;
val.ready = true;
@ -1863,11 +1882,27 @@ u32 rsxaudio_backend_thread::write_data_callback(u32 bytes, void* buf)
return bytes;
}
void rsxaudio_backend_thread::error_callback()
void rsxaudio_backend_thread::state_changed_callback(AudioStateEvent event)
{
{
std::lock_guard lock(state_update_m);
backend_error_occured = true;
switch (event)
{
case AudioStateEvent::UNSPECIFIED_ERROR:
{
backend_error_occured = true;
break;
}
case AudioStateEvent::DEFAULT_DEVICE_CHANGED:
{
backend_device_changed = true;
break;
}
default:
{
fmt::throw_exception("Unknown audio state event");
}
}
}
state_update_c.notify_all();
}

View File

@ -468,6 +468,7 @@ private:
struct emu_audio_cfg
{
std::string audio_device{};
s64 desired_buffer_duration = 0;
f64 time_stretching_threshold = 0;
bool buffering_enabled = false;
@ -548,6 +549,7 @@ private:
backend_config backend_current_cfg{ {}, new_emu_cfg.avport };
atomic_t<callback_config> callback_cfg{};
bool backend_error_occured = false;
bool backend_device_changed = false;
AudioDumper dumper{};
audio_resampler resampler{};
@ -558,7 +560,7 @@ private:
void backend_stop();
bool backend_playing();
u32 write_data_callback(u32 bytes, void* buf);
void error_callback();
void state_changed_callback(AudioStateEvent event);
// Time management
u64 get_time_until_service();

View File

@ -85,6 +85,7 @@ struct EmuCallbacks
std::function<std::shared_ptr<class music_handler_base>()> get_music_handler;
std::function<void(utils::serial*)> init_gs_render;
std::function<std::shared_ptr<class AudioBackend>()> get_audio;
std::function<std::shared_ptr<class audio_device_enumerator>(u64)> get_audio_enumerator; // (audio_renderer)
std::function<std::shared_ptr<class MsgDialogBase>()> get_msg_dialog;
std::function<std::shared_ptr<class OskDialogBase>()> get_osk_dialog;
std::function<std::unique_ptr<class SaveDialogBase>()> get_save_dialog;

View File

@ -253,6 +253,7 @@ struct cfg_root : cfg::node
cfg::_bool convert_to_s16{ this, "Convert to 16 bit", false, true };
cfg::_enum<audio_format> format{ this, "Audio Format", audio_format::stereo, false };
cfg::uint<0, umax> formats{ this, "Audio Formats", static_cast<u32>(audio_format_flag::lpcm_2_48khz), false };
cfg::string audio_device{ this, "Audio Device", "@@@default@@@", true };
cfg::_int<0, 200> volume{ this, "Master Volume", 100, true };
cfg::_bool enable_buffering{ this, "Enable Buffering", true, true };
cfg::_int <4, 250> desired_buffer_duration{ this, "Desired Audio Buffer Duration", 100, true };

View File

@ -53,11 +53,13 @@
</ItemGroup>
<ItemGroup>
<ClInclude Include="Emu\Audio\XAudio2\XAudio2Backend.h" />
<ClInclude Include="Emu\Audio\XAudio2\xaudio2_enumerator.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="Emu\Audio\XAudio2\XAudio2Backend.cpp" />
<ClCompile Include="Emu\Audio\XAudio2\xaudio2_enumerator.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
</Project>

View File

@ -10,10 +10,16 @@
<ClCompile Include="Emu\Audio\XAudio2\XAudio2Backend.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Emu\Audio\XAudio2\xaudio2_enumerator.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="Emu\Audio\XAudio2\XAudio2Backend.h">
<Filter>Source Files</Filter>
</ClInclude>
<ClInclude Include="Emu\Audio\XAudio2\xaudio2_enumerator.h">
<Filter>Source Files</Filter>
</ClInclude>
</ItemGroup>
</Project>
</Project>

View File

@ -53,11 +53,13 @@
<ItemGroup>
<ClCompile Include="..\Utilities\cheat_info.cpp" />
<ClCompile Include="Crypto\decrypt_binaries.cpp" />
<ClCompile Include="Emu\Audio\audio_device_listener.cpp" />
<ClCompile Include="Emu\Audio\audio_resampler.cpp" />
<ClCompile Include="Emu\Audio\FAudio\FAudioBackend.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="Emu\Audio\FAudio\faudio_enumerator.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="Emu\cache_utils.cpp" />
<ClCompile Include="Emu\Cell\Modules\libfs_utility_init.cpp" />
<ClCompile Include="Emu\Cell\Modules\sys_crashdump.cpp" />
@ -456,11 +458,14 @@
<ClInclude Include="..\Utilities\simple_ringbuf.h" />
<ClInclude Include="..\Utilities\transactional_storage.h" />
<ClInclude Include="Crypto\decrypt_binaries.h" />
<ClInclude Include="Emu\Audio\audio_device_listener.h" />
<ClInclude Include="Emu\Audio\audio_resampler.h" />
<ClInclude Include="Emu\Audio\audio_device_enumerator.h" />
<ClInclude Include="Emu\Audio\FAudio\FAudioBackend.h">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClInclude>
<ClInclude Include="Emu\Audio\FAudio\faudio_enumerator.h">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClInclude>
<ClInclude Include="Emu\cache_utils.hpp" />
<ClInclude Include="Emu\Cell\lv2\sys_crypto_engine.h" />
<ClInclude Include="Emu\Cell\Modules\cellCrossController.h" />
@ -618,6 +623,7 @@
<ClInclude Include="Emu\Audio\AudioDumper.h" />
<ClInclude Include="Emu\Audio\AudioBackend.h" />
<ClInclude Include="Emu\Audio\Null\NullAudioBackend.h" />
<ClInclude Include="Emu\Audio\Null\null_enumerator.h" />
<ClInclude Include="Emu\Cell\Common.h" />
<ClInclude Include="Emu\Cell\ErrorCodes.h" />
<ClInclude Include="Emu\Cell\lv2\sys_cond.h" />
@ -835,4 +841,4 @@
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>
</Project>

View File

@ -957,6 +957,9 @@
<ClCompile Include="Emu\Audio\FAudio\FAudioBackend.cpp">
<Filter>Emu\Audio\FAudio</Filter>
</ClCompile>
<ClCompile Include="Emu\Audio\FAudio\faudio_enumerator.cpp">
<Filter>Emu\Audio\FAudio</Filter>
</ClCompile>
<ClCompile Include="..\Utilities\cheat_info.cpp">
<Filter>Utilities</Filter>
</ClCompile>
@ -1045,9 +1048,6 @@
<ClCompile Include="Emu\Cell\Modules\libfs_utility_init.cpp">
<Filter>Emu\Cell\Modules</Filter>
</ClCompile>
<ClCompile Include="Emu\Audio\audio_device_listener.cpp">
<Filter>Emu\Audio</Filter>
</ClCompile>
<ClCompile Include="Emu\vfs_config.cpp">
<Filter>Emu</Filter>
</ClCompile>
@ -1803,6 +1803,9 @@
<ClInclude Include="Emu\Audio\Null\NullAudioBackend.h">
<Filter>Emu\Audio\Null</Filter>
</ClInclude>
<ClInclude Include="Emu\Audio\Null\null_enumerator.h">
<Filter>Emu\Audio\Null</Filter>
</ClInclude>
<ClInclude Include="Emu\Cell\lv2\sys_config.h">
<Filter>Emu\Cell\lv2</Filter>
</ClInclude>
@ -1947,6 +1950,9 @@
<ClInclude Include="Emu\Audio\FAudio\FAudioBackend.h">
<Filter>Emu\Audio\FAudio</Filter>
</ClInclude>
<ClInclude Include="Emu\Audio\FAudio\faudio_enumerator.h">
<Filter>Emu\Audio\FAudio</Filter>
</ClInclude>
<ClInclude Include="..\Utilities\cheat_info.h">
<Filter>Utilities</Filter>
</ClInclude>
@ -2062,9 +2068,6 @@
<ClInclude Include="Emu\Cell\Modules\libfs_utility_init.h">
<Filter>Emu\Cell\Modules</Filter>
</ClInclude>
<ClInclude Include="Emu\Audio\audio_device_listener.h">
<Filter>Emu\Audio</Filter>
</ClInclude>
<ClInclude Include="Emu\vfs_config.h">
<Filter>Emu</Filter>
</ClInclude>
@ -2089,6 +2092,9 @@
<ClInclude Include="Emu\Audio\audio_resampler.h">
<Filter>Emu\Audio</Filter>
</ClInclude>
<ClInclude Include="Emu\Audio\audio_device_enumerator.h">
<Filter>Emu\Audio</Filter>
</ClInclude>
<ClInclude Include="Emu\NP\np_allocator.h">
<Filter>Emu\NP</Filter>
</ClInclude>
@ -2182,4 +2188,4 @@
<Filter>Emu\GPU\RSX\Program\Snippets</Filter>
</None>
</ItemGroup>
</Project>
</Project>

View File

@ -18,12 +18,16 @@
#include "Emu/Audio/AudioBackend.h"
#include "Emu/Audio/Null/NullAudioBackend.h"
#include "Emu/Audio/Null/null_enumerator.h"
#include "Emu/Audio/Cubeb/CubebBackend.h"
#include "Emu/Audio/Cubeb/cubeb_enumerator.h"
#ifdef _WIN32
#include "Emu/Audio/XAudio2/XAudio2Backend.h"
#include "Emu/Audio/XAudio2/xaudio2_enumerator.h"
#endif
#ifdef HAVE_FAUDIO
#include "Emu/Audio/FAudio/FAudioBackend.h"
#include "Emu/Audio/FAudio/faudio_enumerator.h"
#endif
#include <QFileInfo> // This shouldn't be outside rpcs3qt...
@ -127,6 +131,22 @@ EmuCallbacks main_application::CreateCallbacks()
return result;
};
callbacks.get_audio_enumerator = [](u64 renderer) -> std::shared_ptr<audio_device_enumerator>
{
switch (static_cast<audio_renderer>(renderer))
{
case audio_renderer::null: return std::make_shared<null_enumerator>();
#ifdef _WIN32
case audio_renderer::xaudio: return std::make_shared<xaudio2_enumerator>();
#endif
case audio_renderer::cubeb: return std::make_shared<cubeb_enumerator>();
#ifdef HAVE_FAUDIO
case audio_renderer::faudio: return std::make_shared<faudio_enumerator>();
#endif
default: fmt::throw_exception("Invalid renderer index %u", renderer);
}
};
callbacks.resolve_path = [](std::string_view sv)
{
return QFileInfo(QString::fromUtf8(sv.data(), static_cast<int>(sv.size()))).canonicalFilePath().toStdString();

View File

@ -127,6 +127,7 @@ enum class emu_settings_type
AudioFormats,
AudioProvider,
AudioAvport,
AudioDevice,
MasterVolume,
EnableBuffering,
AudioBufferDuration,
@ -302,6 +303,7 @@ inline static const QMap<emu_settings_type, cfg_location> settings_location =
{ emu_settings_type::AudioFormats, { "Audio", "Audio Formats"}},
{ emu_settings_type::AudioProvider, { "Audio", "Audio Provider"}},
{ emu_settings_type::AudioAvport, { "Audio", "RSXAudio Avport"}},
{ emu_settings_type::AudioDevice, { "Audio", "Audio Device"}},
{ emu_settings_type::MasterVolume, { "Audio", "Master Volume"}},
{ emu_settings_type::EnableBuffering, { "Audio", "Enable Buffering"}},
{ emu_settings_type::AudioBufferDuration, { "Audio", "Desired Audio Buffer Duration"}},

View File

@ -29,6 +29,8 @@
#include "Emu/system_config.h"
#include "Emu/title.h"
#include "Emu/Audio/audio_device_enumerator.h"
#include "Loader/PSF.h"
#include <set>
@ -871,31 +873,6 @@ settings_dialog::settings_dialog(std::shared_ptr<gui_settings> gui_settings, std
// / ____ \ |_| | (_| | | (_) | | | (_| | |_) |
// /_/ \_\__,_|\__,_|_|\___/ |_|\__,_|_.__/
const auto enable_time_stretching_options = [this](bool enabled)
{
ui->timeStretchingThresholdLabel->setEnabled(enabled);
ui->timeStretchingThreshold->setEnabled(enabled);
};
const auto enable_buffering_options = [this, enable_time_stretching_options](bool enabled)
{
ui->audioBufferDuration->setEnabled(enabled);
ui->audioBufferDurationLabel->setEnabled(enabled);
ui->enableTimeStretching->setEnabled(enabled);
enable_time_stretching_options(enabled && ui->enableTimeStretching->isChecked());
};
const auto enable_buffering = [this, enable_buffering_options](int index)
{
if (index < 0) return;
const QVariantList var_list = ui->audioOutBox->itemData(index).toList();
ensure(var_list.size() == 2 && var_list[0].canConvert<QString>());
const QString text = var_list[0].toString();
const bool enabled = text == "Cubeb" || text == "XAudio2" || text == "FAudio";
ui->enableBuffering->setEnabled(enabled);
enable_buffering_options(enabled && ui->enableBuffering->isChecked());
};
const QString mic_none = m_emu_settings->m_microphone_creator.get_none();
const auto change_microphone_type = [mic_none, this](int index)
@ -960,6 +937,45 @@ settings_dialog::settings_dialog(std::shared_ptr<gui_settings> gui_settings, std
propagate_used_devices();
};
const auto get_audio_output_devices = [this]()
{
const QVariantList var_list = ui->audioOutBox->currentData().toList();
ensure(var_list.size() == 2 && var_list[1].canConvert<int>());
auto dev_enum = Emu.GetCallbacks().get_audio_enumerator(var_list[1].toInt());
std::vector<audio_device_enumerator::audio_device> dev_array = dev_enum->get_output_devices();
ui->audioDeviceBox->clear();
ui->audioDeviceBox->blockSignals(true);
ui->audioDeviceBox->addItem(tr("Default"), qsv(audio_device_enumerator::DEFAULT_DEV_ID));
int device_index = 0;
for (auto& dev : dev_array)
{
const QString cur_item = qstr(dev.id);
ui->audioDeviceBox->addItem(qstr(dev.name), cur_item);
if (g_cfg.audio.audio_device.to_string() == dev.id)
{
device_index = ui->audioDeviceBox->findData(cur_item);
}
}
ui->audioDeviceBox->blockSignals(false);
ui->audioDeviceBox->setCurrentIndex(std::max(device_index, 0));
};
const auto change_audio_output_device = [this](int index)
{
if (index < 0)
{
return;
}
const QVariant item_data = ui->audioDeviceBox->itemData(index);
m_emu_settings->SetSetting(emu_settings_type::AudioDevice, sstr(item_data.toString()));
ui->audioDeviceBox->setCurrentIndex(index);
};
// Comboboxes
m_emu_settings->EnhanceComboBox(ui->audioOutBox, emu_settings_type::AudioRenderer);
@ -968,7 +984,11 @@ settings_dialog::settings_dialog(std::shared_ptr<gui_settings> gui_settings, std
#else
SubscribeTooltip(ui->gb_audio_out, tooltips.settings.audio_out_linux);
#endif
connect(ui->audioOutBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, enable_buffering);
connect(ui->audioOutBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [change_audio_output_device, get_audio_output_devices](int)
{
get_audio_output_devices();
change_audio_output_device(0); // Set device to 'Default'
});
connect(ui->combo_audio_format, QOverload<int>::of(&QComboBox::currentIndexChanged), this, [this](int index)
{
@ -1035,6 +1055,11 @@ settings_dialog::settings_dialog(std::shared_ptr<gui_settings> gui_settings, std
m_emu_settings->EnhanceComboBox(ui->audioAvportBox, emu_settings_type::AudioAvport);
SubscribeTooltip(ui->gb_audio_avport, tooltips.settings.audio_avport);
SubscribeTooltip(ui->gb_audio_device, tooltips.settings.audio_device);
connect(ui->audioDeviceBox, QOverload<int>::of(&QComboBox::currentIndexChanged), this, change_audio_output_device);
connect(this, &settings_dialog::signal_restore_dependant_defaults, this, [change_audio_output_device]() { change_audio_output_device(0); }); // Set device to 'Default'
get_audio_output_devices();
// Microphone Comboboxes
m_mics_combo[0] = ui->microphone1Box;
m_mics_combo[1] = ui->microphone2Box;
@ -1084,13 +1109,9 @@ settings_dialog::settings_dialog(std::shared_ptr<gui_settings> gui_settings, std
m_emu_settings->EnhanceCheckBox(ui->enableBuffering, emu_settings_type::EnableBuffering);
SubscribeTooltip(ui->enableBuffering, tooltips.settings.enable_buffering);
connect(ui->enableBuffering, &QCheckBox::toggled, enable_buffering_options);
m_emu_settings->EnhanceCheckBox(ui->enableTimeStretching, emu_settings_type::EnableTimeStretching);
SubscribeTooltip(ui->enableTimeStretching, tooltips.settings.enable_time_stretching);
connect(ui->enableTimeStretching, &QCheckBox::toggled, enable_time_stretching_options);
enable_buffering(ui->audioOutBox->currentIndex());
// Sliders
@ -1315,10 +1336,10 @@ settings_dialog::settings_dialog(std::shared_ptr<gui_settings> gui_settings, std
m_emu_settings->EnhanceCheckBox(ui->disableOnDiskShaderCache, emu_settings_type::DisableOnDiskShaderCache);
SubscribeTooltip(ui->disableOnDiskShaderCache, tooltips.settings.disable_on_disk_shader_cache);
m_emu_settings->EnhanceCheckBox(ui->forceDisableExclusiveFullscreenMode, emu_settings_type::ForceDisableExclusiveFullscreenMode);
SubscribeTooltip(ui->forceDisableExclusiveFullscreenMode, tooltips.settings.force_disable_exclusive_fullscreen_mode);
m_emu_settings->EnhanceCheckBox(ui->vblankNTSCFixup, emu_settings_type::VBlankNTSCFixup);
ui->mfcDelayCommand->setChecked(m_emu_settings->GetSetting(emu_settings_type::MFCCommandsShuffling) == "1");

View File

@ -1114,6 +1114,18 @@
</item>
<item>
<layout class="QVBoxLayout" name="audioTabLayoutTopMiddle">
<item>
<widget class="QGroupBox" name="gb_audio_device">
<property name="title">
<string>Audio Device</string>
</property>
<layout class="QVBoxLayout" name="gb_audio_device_layout">
<item>
<widget class="QComboBox" name="audioDeviceBox"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="gb_audio_provider">
<property name="title">

View File

@ -50,6 +50,7 @@ public:
const QString audio_out_linux = tr("Cubeb uses a cross-platform approach and supports audio buffering, so it is the recommended option.\nIf it's not available, FAudio could be used instead.");
const QString audio_provider = tr("Controls which PS3 audio API is used.\nGames use CellAudio, while VSH requires RSXAudio.");
const QString audio_avport = tr("Controls which avport is used to sample audio data from.");
const QString audio_device = tr("Controls which device is used by audio backend.");
const QString audio_dump = tr("Saves all audio as a raw wave file. If unsure, leave this unchecked.");
const QString convert = tr("Uses 16-bit audio samples instead of default 32-bit floating point.\nUse with buggy audio drivers if you have no sound or completely broken sound.");
const QString audio_format = tr("Determines the sound format.\nConfigure this setting if you want to switch between stereo and surround sound.\nChanging these values requires a restart of the game.\nThe manual setting will use your selected formats while the automatic setting will let the game choose from all available formats.");