From 98b730c806a05b4d16c5e062ecce57adc0a19c2f Mon Sep 17 00:00:00 2001 From: Vestrel <16190165+Vestrel@users.noreply.github.com> Date: Sat, 9 Jul 2022 00:13:38 +0900 Subject: [PATCH] Audio: device switching and channel count detection (#12246) --- Utilities/StrFmt.cpp | 12 +- Utilities/StrUtil.h | 1 + rpcs3/Cubeb.vcxproj | 4 +- rpcs3/Cubeb.vcxproj.filters | 6 + rpcs3/Emu/Audio/AudioBackend.cpp | 33 ++- rpcs3/Emu/Audio/AudioBackend.h | 33 ++- rpcs3/Emu/Audio/Cubeb/CubebBackend.cpp | 276 +++++++++++++++--- rpcs3/Emu/Audio/Cubeb/CubebBackend.h | 18 +- rpcs3/Emu/Audio/Cubeb/cubeb_enumerator.cpp | 113 +++++++ rpcs3/Emu/Audio/Cubeb/cubeb_enumerator.h | 22 ++ rpcs3/Emu/Audio/FAudio/FAudioBackend.cpp | 75 +++-- rpcs3/Emu/Audio/FAudio/FAudioBackend.h | 7 +- rpcs3/Emu/Audio/FAudio/faudio_enumerator.cpp | 90 ++++++ rpcs3/Emu/Audio/FAudio/faudio_enumerator.h | 22 ++ rpcs3/Emu/Audio/Null/NullAudioBackend.h | 5 +- rpcs3/Emu/Audio/Null/null_enumerator.h | 13 + rpcs3/Emu/Audio/XAudio2/XAudio2Backend.cpp | 225 +++++++++++--- rpcs3/Emu/Audio/XAudio2/XAudio2Backend.h | 30 +- .../Emu/Audio/XAudio2/xaudio2_enumerator.cpp | 153 ++++++++++ rpcs3/Emu/Audio/XAudio2/xaudio2_enumerator.h | 17 ++ rpcs3/Emu/Audio/audio_device_enumerator.h | 26 ++ rpcs3/Emu/Audio/audio_device_listener.cpp | 147 ---------- rpcs3/Emu/Audio/audio_device_listener.h | 42 --- rpcs3/Emu/CMakeLists.txt | 8 +- rpcs3/Emu/Cell/Modules/cellAudio.cpp | 27 +- rpcs3/Emu/Cell/Modules/cellAudio.h | 6 + rpcs3/Emu/Cell/lv2/sys_rsxaudio.cpp | 49 +++- rpcs3/Emu/Cell/lv2/sys_rsxaudio.h | 4 +- rpcs3/Emu/System.h | 1 + rpcs3/Emu/system_config.h | 1 + rpcs3/XAudio.vcxproj | 4 +- rpcs3/XAudio.vcxproj.filters | 8 +- rpcs3/emucore.vcxproj | 12 +- rpcs3/emucore.vcxproj.filters | 20 +- rpcs3/main_application.cpp | 20 ++ rpcs3/rpcs3qt/emu_settings_type.h | 2 + rpcs3/rpcs3qt/settings_dialog.cpp | 85 ++++-- rpcs3/rpcs3qt/settings_dialog.ui | 12 + rpcs3/rpcs3qt/tooltips.h | 1 + 39 files changed, 1235 insertions(+), 395 deletions(-) create mode 100644 rpcs3/Emu/Audio/Cubeb/cubeb_enumerator.cpp create mode 100644 rpcs3/Emu/Audio/Cubeb/cubeb_enumerator.h create mode 100644 rpcs3/Emu/Audio/FAudio/faudio_enumerator.cpp create mode 100644 rpcs3/Emu/Audio/FAudio/faudio_enumerator.h create mode 100644 rpcs3/Emu/Audio/Null/null_enumerator.h create mode 100644 rpcs3/Emu/Audio/XAudio2/xaudio2_enumerator.cpp create mode 100644 rpcs3/Emu/Audio/XAudio2/xaudio2_enumerator.h create mode 100644 rpcs3/Emu/Audio/audio_device_enumerator.h delete mode 100644 rpcs3/Emu/Audio/audio_device_listener.cpp delete mode 100644 rpcs3/Emu/Audio/audio_device_listener.h diff --git a/Utilities/StrFmt.cpp b/Utilities/StrFmt.cpp index d462c69974..9972bc7e8c 100644 --- a/Utilities/StrFmt.cpp +++ b/Utilities/StrFmt.cpp @@ -5,6 +5,8 @@ #include "util/logs.hpp" #include "util/v128.hpp" +#include +#include #include #include #include "Thread.h" @@ -13,8 +15,6 @@ #include #else #include -#include -#include #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, wchar_t> converter{}; + std::wstring_convert, wchar_t> converter{}; return converter.to_bytes(src.data()); #endif } +std::string utf16_to_utf8(std::u16string_view src) +{ + std::wstring_convert, char16_t> converter{}; + return converter.to_bytes(src.data()); +} + std::wstring utf8_to_wchar(std::string_view src) { #ifdef _WIN32 diff --git a/Utilities/StrUtil.h b/Utilities/StrUtil.h index 643bc7909b..2d0fcfe742 100644 --- a/Utilities/StrUtil.h +++ b/Utilities/StrUtil.h @@ -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 diff --git a/rpcs3/Cubeb.vcxproj b/rpcs3/Cubeb.vcxproj index c46472f9e4..05a6f461b3 100644 --- a/rpcs3/Cubeb.vcxproj +++ b/rpcs3/Cubeb.vcxproj @@ -53,11 +53,13 @@ + + - \ No newline at end of file + diff --git a/rpcs3/Cubeb.vcxproj.filters b/rpcs3/Cubeb.vcxproj.filters index 9c799be1ff..396ad1527d 100644 --- a/rpcs3/Cubeb.vcxproj.filters +++ b/rpcs3/Cubeb.vcxproj.filters @@ -10,10 +10,16 @@ Source Files + + Source Files + Source Files + + Source Files + diff --git a/rpcs3/Emu/Audio/AudioBackend.cpp b/rpcs3/Emu/Audio/AudioBackend.cpp index 696b8fac9b..e6386c1b50 100644 --- a/rpcs3/Emu/Audio/AudioBackend.cpp +++ b/rpcs3/Emu/Audio/AudioBackend.cpp @@ -6,10 +6,16 @@ AudioBackend::AudioBackend() {} -void AudioBackend::SetErrorCallback(std::function cb) +void AudioBackend::SetWriteCallback(std::function 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 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"); + } +} diff --git a/rpcs3/Emu/Audio/AudioBackend.h b/rpcs3/Emu/Audio/AudioBackend.h index 40e3b6bf98..f1adbcae9c 100644 --- a/rpcs3/Emu/Audio/AudioBackend.h +++ b/rpcs3/Emu/Audio/AudioBackend.h @@ -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 cb) = 0; + virtual void SetWriteCallback(std::function 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 cb); + virtual void SetStateCallback(std::function 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 m_error_callback{}; + shared_mutex m_cb_mutex{}; + std::function m_write_callback{}; + + shared_mutex m_state_cb_mutex{}; + std::function m_state_callback{}; bool m_playing = false; diff --git a/rpcs3/Emu/Audio/Cubeb/CubebBackend.cpp b/rpcs3/Emu/Audio/Cubeb/CubebBackend.cpp index 12fb162fdf..5b87242205 100644 --- a/rpcs3/Emu/Audio/Cubeb/CubebBackend.cpp +++ b/rpcs3/Emu/Audio/Cubeb/CubebBackend.cpp @@ -2,6 +2,7 @@ #include #include "util/logs.hpp" +#include "Emu/Audio/audio_device_enumerator.h" #ifdef _WIN32 #include @@ -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(std::min(static_cast(convert_channel_count(dev_ch_cnt)), static_cast(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(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 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(AUDIO_MIN_LATENCY, static_cast(stream_latency) / get_sampling_rate()); } +std::tuple 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 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 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(freq), + .channels = static_cast(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, ¶m, &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, ¶m, 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(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(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); } } } diff --git a/rpcs3/Emu/Audio/Cubeb/CubebBackend.h b/rpcs3/Emu/Audio/Cubeb/CubebBackend.h index d6e3ddb056..1f6330dec5 100644 --- a/rpcs3/Emu/Audio/Cubeb/CubebBackend.h +++ b/rpcs3/Emu/Audio/Cubeb/CubebBackend.h @@ -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 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 m_write_callback{}; std::array(AudioChannelCnt::SURROUND_7_1)> m_last_sample{}; atomic_t full_sample_size = 0; - bool m_reset_req = false; + atomic_t 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 GetDevice(std::string_view dev_id = ""); + std::tuple GetDefaultDeviceAlt(AudioFreq freq, AudioSampleSize sample_size, AudioChannelCnt ch_cnt); }; diff --git a/rpcs3/Emu/Audio/Cubeb/cubeb_enumerator.cpp b/rpcs3/Emu/Audio/Cubeb/cubeb_enumerator.cpp new file mode 100644 index 0000000000..877628d5d0 --- /dev/null +++ b/rpcs3/Emu/Audio/Cubeb/cubeb_enumerator.cpp @@ -0,0 +1,113 @@ +#include "Emu/Audio/Cubeb/cubeb_enumerator.h" +#include "util/logs.hpp" +#include + +#ifdef _WIN32 +#include +#include +#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 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 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; +} diff --git a/rpcs3/Emu/Audio/Cubeb/cubeb_enumerator.h b/rpcs3/Emu/Audio/Cubeb/cubeb_enumerator.h new file mode 100644 index 0000000000..586521028f --- /dev/null +++ b/rpcs3/Emu/Audio/Cubeb/cubeb_enumerator.h @@ -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 get_output_devices() override; + +private: + + cubeb* ctx{}; +#ifdef _WIN32 + bool com_init_success = false; +#endif +}; diff --git a/rpcs3/Emu/Audio/FAudio/FAudioBackend.cpp b/rpcs3/Emu/Audio/FAudio/FAudioBackend.cpp index b4dfc5f906..7e9efb1d9a 100644 --- a/rpcs3/Emu/Audio/FAudio/FAudioBackend.cpp +++ b/rpcs3/Emu/Audio/FAudio/FAudioBackend.cpp @@ -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(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(std::min(static_cast(convert_channel_count(vd.InputChannels)), static_cast(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(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 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(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(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); } } diff --git a/rpcs3/Emu/Audio/FAudio/FAudioBackend.h b/rpcs3/Emu/Audio/FAudio/FAudioBackend.h index 98fd4251e7..66802a8bf5 100644 --- a/rpcs3/Emu/Audio/FAudio/FAudioBackend.h +++ b/rpcs3/Emu/Audio/FAudio/FAudioBackend.h @@ -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 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 m_write_callback{}; std::vector m_data_buf{}; std::array(AudioChannelCnt::SURROUND_7_1)> m_last_sample{}; - bool m_reset_req = false; + atomic_t m_reset_req = false; // FAudio voice callbacks static void OnVoiceProcessingPassStart_func(FAudioVoiceCallback *cb_obj, u32 BytesRequired); diff --git a/rpcs3/Emu/Audio/FAudio/faudio_enumerator.cpp b/rpcs3/Emu/Audio/FAudio/faudio_enumerator.cpp new file mode 100644 index 0000000000..7308f54b19 --- /dev/null +++ b/rpcs3/Emu/Audio/FAudio/faudio_enumerator.cpp @@ -0,0 +1,90 @@ +#ifndef HAVE_FAUDIO +#error "FAudio support disabled but still being built." +#endif + +#include "Emu/Audio/FAudio/faudio_enumerator.h" +#include +#include +#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 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 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(&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; +} diff --git a/rpcs3/Emu/Audio/FAudio/faudio_enumerator.h b/rpcs3/Emu/Audio/FAudio/faudio_enumerator.h new file mode 100644 index 0000000000..406bfa09a4 --- /dev/null +++ b/rpcs3/Emu/Audio/FAudio/faudio_enumerator.h @@ -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 get_output_devices() override; + +private: + + FAudio* instance{}; +}; diff --git a/rpcs3/Emu/Audio/Null/NullAudioBackend.h b/rpcs3/Emu/Audio/Null/NullAudioBackend.h index 70b02bfde4..3afdeafb79 100644 --- a/rpcs3/Emu/Audio/Null/NullAudioBackend.h +++ b/rpcs3/Emu/Audio/Null/NullAudioBackend.h @@ -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 /* cb */) override {}; f64 GetCallbackFrameLen() override { return 0.01; }; - void SetErrorCallback(std::function /* cb */) override {}; - void Play() override { m_playing = true; } void Pause() override { m_playing = false; } bool IsPlaying() override { return m_playing; } diff --git a/rpcs3/Emu/Audio/Null/null_enumerator.h b/rpcs3/Emu/Audio/Null/null_enumerator.h new file mode 100644 index 0000000000..9a889cdcee --- /dev/null +++ b/rpcs3/Emu/Audio/Null/null_enumerator.h @@ -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 get_output_devices() override { return {}; } +}; diff --git a/rpcs3/Emu/Audio/XAudio2/XAudio2Backend.cpp b/rpcs3/Emu/Audio/XAudio2/XAudio2Backend.cpp index c02f253eaa..9b1c9211e0 100644 --- a/rpcs3/Emu/Audio/XAudio2/XAudio2Backend.cpp +++ b/rpcs3/Emu/Audio/XAudio2/XAudio2Backend.cpp @@ -5,6 +5,8 @@ #include #include "util/logs.hpp" #include "Emu/System.h" +#include "Emu/Audio/audio_device_enumerator.h" +#include "Utilities/StrUtil.h" #include "XAudio2Backend.h" #include @@ -14,42 +16,91 @@ LOG_CHANNEL(XAudio); +template <> +void fmt_class_string::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::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 instance; + Microsoft::WRL::ComPtr instance{}; + Microsoft::WRL::ComPtr 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(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(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(hr)); + return; + } + + if (HRESULT hr = enumerator->RegisterEndpointNotificationCallback(this); FAILED(hr)) + { + XAudio.error("RegisterEndpointNotificationCallback() failed: %s (0x%08x)", std::system_category().message(hr), static_cast(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 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(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(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(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(std::min(static_cast(convert_channel_count(vd.InputChannels)), static_cast(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(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(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(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(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(XAUDIO2_DEFAULT_FREQ_RATIO) / 1000); - return true; } -void XAudio2Backend::SetWriteCallback(std::function 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 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(xaudio_ext.GetAddressOf())); FAILED(hr)) { XAudio.error("QueryInterface() failed: %s (0x%08x)", std::system_category().message(hr), static_cast(hr)); } else { u32 samples_per_q = 0, freq = 0; - static_cast(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(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; +} diff --git a/rpcs3/Emu/Audio/XAudio2/XAudio2Backend.h b/rpcs3/Emu/Audio/XAudio2/XAudio2Backend.h index ef9f658afc..81a7e8502d 100644 --- a/rpcs3/Emu/Audio/XAudio2/XAudio2Backend.h +++ b/rpcs3/Emu/Audio/XAudio2/XAudio2Backend.h @@ -7,12 +7,12 @@ #include #include "Utilities/mutex.h" #include "Emu/Audio/AudioBackend.h" -#include "Emu/Audio/audio_device_listener.h" #include #include +#include -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 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 m_write_callback{}; + Microsoft::WRL::ComPtr m_device_enumerator{}; + + shared_mutex m_dev_sw_mutex{}; + std::string m_current_device{}; + bool m_default_dev_changed = false; + std::vector m_data_buf{}; std::array(AudioChannelCnt::SURROUND_7_1)> m_last_sample{}; - bool m_reset_req = false; - - audio_device_listener m_dev_listener{}; + atomic_t 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(); }; diff --git a/rpcs3/Emu/Audio/XAudio2/xaudio2_enumerator.cpp b/rpcs3/Emu/Audio/XAudio2/xaudio2_enumerator.cpp new file mode 100644 index 0000000000..5f1fd1f44c --- /dev/null +++ b/rpcs3/Emu/Audio/XAudio2/xaudio2_enumerator.cpp @@ -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 + +#include +#include +#include +#include +#include + +LOG_CHANNEL(xaudio_dev_enum); + +xaudio2_enumerator::xaudio2_enumerator() : audio_device_enumerator() +{ +} + +xaudio2_enumerator::~xaudio2_enumerator() +{ +} + +std::vector xaudio2_enumerator::get_output_devices() +{ + Microsoft::WRL::ComPtr 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(hr)); + return {}; + } + + Microsoft::WRL::ComPtr 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(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(hr)); + return {}; + } + + if (count == 0) + { + xaudio_dev_enum.warning("No devices available"); + return {}; + } + + std::vector device_list{}; + + for (UINT dev_idx = 0; dev_idx < count; dev_idx++) + { + Microsoft::WRL::ComPtr 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(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(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 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(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(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(var.blob.pBlobData); + dev.max_ch = pcm->wf.nChannels; + } + else if (var.blob.cbSize >= sizeof(WAVEFORMATEX)) + { + const WAVEFORMATEX* wfx = std::bit_cast(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(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; +} diff --git a/rpcs3/Emu/Audio/XAudio2/xaudio2_enumerator.h b/rpcs3/Emu/Audio/XAudio2/xaudio2_enumerator.h new file mode 100644 index 0000000000..e96e3fe9b5 --- /dev/null +++ b/rpcs3/Emu/Audio/XAudio2/xaudio2_enumerator.h @@ -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 get_output_devices() override; +}; diff --git a/rpcs3/Emu/Audio/audio_device_enumerator.h b/rpcs3/Emu/Audio/audio_device_enumerator.h new file mode 100644 index 0000000000..6c81dedee7 --- /dev/null +++ b/rpcs3/Emu/Audio/audio_device_enumerator.h @@ -0,0 +1,26 @@ +#pragma once + +#include "util/types.hpp" +#include +#include + +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 get_output_devices() = 0; +}; diff --git a/rpcs3/Emu/Audio/audio_device_listener.cpp b/rpcs3/Emu/Audio/audio_device_listener.cpp deleted file mode 100644 index 3bf12e82c0..0000000000 --- a/rpcs3/Emu/Audio/audio_device_listener.cpp +++ /dev/null @@ -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(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(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::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::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 diff --git a/rpcs3/Emu/Audio/audio_device_listener.h b/rpcs3/Emu/Audio/audio_device_listener.h deleted file mode 100644 index 947d7ec6d9..0000000000 --- a/rpcs3/Emu/Audio/audio_device_listener.h +++ /dev/null @@ -1,42 +0,0 @@ -#pragma once - -#ifdef _WIN32 -#include -#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 m_input_device_changed = false; - atomic_t m_output_device_changed = false; -}; diff --git a/rpcs3/Emu/CMakeLists.txt b/rpcs3/Emu/CMakeLists.txt index 6ce5f17a3b..70770321a9 100644 --- a/rpcs3/Emu/CMakeLists.txt +++ b/rpcs3/Emu/CMakeLists.txt @@ -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() diff --git a/rpcs3/Emu/Cell/Modules/cellAudio.cpp b/rpcs3/Emu/Cell/Modules/cellAudio.cpp index fdce713adf..409646a53e 100644 --- a/rpcs3/Emu/Cell/Modules/cellAudio.cpp +++ b/rpcs3/Emu/Cell/Modules/cellAudio.cpp @@ -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(ch_cnt); + audio_channels = ch_cnt; audio_sampling_rate = static_cast(freq); audio_block_period = AUDIO_BUFFER_SAMPLES * 1'000'000 / audio_sampling_rate; audio_sample_size = static_cast(sample_size); @@ -567,6 +579,7 @@ namespace audio { return { + .audio_device = g_cfg.audio.audio_device, .buffering_enabled = static_cast(g_cfg.audio.enable_buffering), .desired_buffer_duration = g_cfg.audio.desired_buffer_duration, .enable_time_stretching = static_cast(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"); diff --git a/rpcs3/Emu/Cell/Modules/cellAudio.h b/rpcs3/Emu/Cell/Modules/cellAudio.h index 5e1c47798e..7feb4e3dc4 100644 --- a/rpcs3/Emu/Cell/Modules/cellAudio.h +++ b/rpcs3/Emu/Cell/Modules/cellAudio.h @@ -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(); diff --git a/rpcs3/Emu/Cell/lv2/sys_rsxaudio.cpp b/rpcs3/Emu/Cell/lv2/sys_rsxaudio.cpp index a670c72d71..a2eb76dc8e 100644 --- a/rpcs3/Emu/Cell/lv2/sys_rsxaudio.cpp +++ b/rpcs3/Emu/Cell/lv2/sys_rsxaudio.cpp @@ -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(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(emu_cfg.avport)]; const AudioSampleSize sample_size = emu_cfg.convert_to_s16 ? AudioSampleSize::S16 : AudioSampleSize::FLOAT; const AudioChannelCnt ch_cnt = static_cast(std::min(static_cast(port_cfg.ch_cnt), static_cast(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(AudioSampleSize::FLOAT) * static_cast(port_cfg.ch_cnt) * static_cast(port_cfg.freq); @@ -1711,7 +1730,7 @@ void rsxaudio_backend_thread::backend_init(const rsxaudio_state& ra_state, const { val.freq = static_cast(port_cfg.freq); val.input_ch_cnt = static_cast(port_cfg.ch_cnt); - val.output_ch_cnt = static_cast(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(); } diff --git a/rpcs3/Emu/Cell/lv2/sys_rsxaudio.h b/rpcs3/Emu/Cell/lv2/sys_rsxaudio.h index 08e88c7ef5..66088762be 100644 --- a/rpcs3/Emu/Cell/lv2/sys_rsxaudio.h +++ b/rpcs3/Emu/Cell/lv2/sys_rsxaudio.h @@ -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_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(); diff --git a/rpcs3/Emu/System.h b/rpcs3/Emu/System.h index 8445c7fc69..8e4772aa58 100644 --- a/rpcs3/Emu/System.h +++ b/rpcs3/Emu/System.h @@ -85,6 +85,7 @@ struct EmuCallbacks std::function()> get_music_handler; std::function init_gs_render; std::function()> get_audio; + std::function(u64)> get_audio_enumerator; // (audio_renderer) std::function()> get_msg_dialog; std::function()> get_osk_dialog; std::function()> get_save_dialog; diff --git a/rpcs3/Emu/system_config.h b/rpcs3/Emu/system_config.h index 858a5c3079..9038f67de1 100644 --- a/rpcs3/Emu/system_config.h +++ b/rpcs3/Emu/system_config.h @@ -253,6 +253,7 @@ struct cfg_root : cfg::node cfg::_bool convert_to_s16{ this, "Convert to 16 bit", false, true }; cfg::_enum format{ this, "Audio Format", audio_format::stereo, false }; cfg::uint<0, umax> formats{ this, "Audio Formats", static_cast(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 }; diff --git a/rpcs3/XAudio.vcxproj b/rpcs3/XAudio.vcxproj index 654031f66f..97dc0b68cf 100644 --- a/rpcs3/XAudio.vcxproj +++ b/rpcs3/XAudio.vcxproj @@ -53,11 +53,13 @@ + + - \ No newline at end of file + diff --git a/rpcs3/XAudio.vcxproj.filters b/rpcs3/XAudio.vcxproj.filters index bb23de38fe..78e33e85ce 100644 --- a/rpcs3/XAudio.vcxproj.filters +++ b/rpcs3/XAudio.vcxproj.filters @@ -10,10 +10,16 @@ Source Files + + Source Files + Source Files + + Source Files + - \ No newline at end of file + diff --git a/rpcs3/emucore.vcxproj b/rpcs3/emucore.vcxproj index bd423171d9..1b1e13da83 100644 --- a/rpcs3/emucore.vcxproj +++ b/rpcs3/emucore.vcxproj @@ -53,11 +53,13 @@ - true + + true + @@ -456,11 +458,14 @@ - + true + + true + @@ -618,6 +623,7 @@ + @@ -835,4 +841,4 @@ - \ No newline at end of file + diff --git a/rpcs3/emucore.vcxproj.filters b/rpcs3/emucore.vcxproj.filters index ce41a00dbe..521a02be39 100644 --- a/rpcs3/emucore.vcxproj.filters +++ b/rpcs3/emucore.vcxproj.filters @@ -957,6 +957,9 @@ Emu\Audio\FAudio + + Emu\Audio\FAudio + Utilities @@ -1045,9 +1048,6 @@ Emu\Cell\Modules - - Emu\Audio - Emu @@ -1803,6 +1803,9 @@ Emu\Audio\Null + + Emu\Audio\Null + Emu\Cell\lv2 @@ -1947,6 +1950,9 @@ Emu\Audio\FAudio + + Emu\Audio\FAudio + Utilities @@ -2062,9 +2068,6 @@ Emu\Cell\Modules - - Emu\Audio - Emu @@ -2089,6 +2092,9 @@ Emu\Audio + + Emu\Audio + Emu\NP @@ -2182,4 +2188,4 @@ Emu\GPU\RSX\Program\Snippets - \ No newline at end of file + diff --git a/rpcs3/main_application.cpp b/rpcs3/main_application.cpp index 490517cf3e..c4273e6f2d 100644 --- a/rpcs3/main_application.cpp +++ b/rpcs3/main_application.cpp @@ -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 // 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 + { + switch (static_cast(renderer)) + { + case audio_renderer::null: return std::make_shared(); +#ifdef _WIN32 + case audio_renderer::xaudio: return std::make_shared(); +#endif + case audio_renderer::cubeb: return std::make_shared(); +#ifdef HAVE_FAUDIO + case audio_renderer::faudio: return std::make_shared(); +#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(sv.size()))).canonicalFilePath().toStdString(); diff --git a/rpcs3/rpcs3qt/emu_settings_type.h b/rpcs3/rpcs3qt/emu_settings_type.h index bd63c955a8..373121e473 100644 --- a/rpcs3/rpcs3qt/emu_settings_type.h +++ b/rpcs3/rpcs3qt/emu_settings_type.h @@ -127,6 +127,7 @@ enum class emu_settings_type AudioFormats, AudioProvider, AudioAvport, + AudioDevice, MasterVolume, EnableBuffering, AudioBufferDuration, @@ -302,6 +303,7 @@ inline static const QMap 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"}}, diff --git a/rpcs3/rpcs3qt/settings_dialog.cpp b/rpcs3/rpcs3qt/settings_dialog.cpp index 3e72ca59d0..f6f0e4d7a3 100644 --- a/rpcs3/rpcs3qt/settings_dialog.cpp +++ b/rpcs3/rpcs3qt/settings_dialog.cpp @@ -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 @@ -871,31 +873,6 @@ settings_dialog::settings_dialog(std::shared_ptr 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()); - 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, 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()); + auto dev_enum = Emu.GetCallbacks().get_audio_enumerator(var_list[1].toInt()); + std::vector 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, std #else SubscribeTooltip(ui->gb_audio_out, tooltips.settings.audio_out_linux); #endif - connect(ui->audioOutBox, QOverload::of(&QComboBox::currentIndexChanged), this, enable_buffering); + connect(ui->audioOutBox, QOverload::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::of(&QComboBox::currentIndexChanged), this, [this](int index) { @@ -1035,6 +1055,11 @@ settings_dialog::settings_dialog(std::shared_ptr 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::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, 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, 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"); diff --git a/rpcs3/rpcs3qt/settings_dialog.ui b/rpcs3/rpcs3qt/settings_dialog.ui index 9ca8dce7b7..25d7d15a98 100644 --- a/rpcs3/rpcs3qt/settings_dialog.ui +++ b/rpcs3/rpcs3qt/settings_dialog.ui @@ -1114,6 +1114,18 @@ + + + + Audio Device + + + + + + + + diff --git a/rpcs3/rpcs3qt/tooltips.h b/rpcs3/rpcs3qt/tooltips.h index d91b13ecc4..148266512b 100644 --- a/rpcs3/rpcs3qt/tooltips.h +++ b/rpcs3/rpcs3qt/tooltips.h @@ -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.");