mirror of
https://github.com/RPCS3/rpcs3.git
synced 2024-11-25 04:02:42 +01:00
Audio: device switching and channel count detection (#12246)
This commit is contained in:
parent
4b787b22c8
commit
98b730c806
@ -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
|
||||
|
@ -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>
|
||||
|
@ -53,9 +53,11 @@
|
||||
</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">
|
||||
|
@ -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>
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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, ¶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<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,11 +20,11 @@ public:
|
||||
|
||||
bool Initialized() override;
|
||||
bool Operational() override;
|
||||
bool DefaultDeviceChanged() override;
|
||||
|
||||
bool Open(AudioFreq freq, AudioSampleSize sample_size, AudioChannelCnt ch_cnt) override;
|
||||
bool Open(std::string_view dev_id, AudioFreq freq, AudioSampleSize sample_size, AudioChannelCnt ch_cnt) override;
|
||||
void Close() override;
|
||||
|
||||
void SetWriteCallback(std::function<u32(u32, void *)> cb) override;
|
||||
f64 GetCallbackFrameLen() override;
|
||||
|
||||
void Play() override;
|
||||
@ -39,16 +39,24 @@ private:
|
||||
bool m_com_init_success = false;
|
||||
#endif
|
||||
|
||||
shared_mutex m_cb_mutex{};
|
||||
std::function<u32(u32, void *)> m_write_callback{};
|
||||
std::array<u8, sizeof(float) * static_cast<u32>(AudioChannelCnt::SURROUND_7_1)> m_last_sample{};
|
||||
atomic_t<u8> full_sample_size = 0;
|
||||
|
||||
bool m_reset_req = false;
|
||||
atomic_t<bool> m_reset_req = false;
|
||||
|
||||
shared_mutex m_dev_sw_mutex{};
|
||||
std::string m_current_device{};
|
||||
bool m_default_dev_changed = false;
|
||||
|
||||
bool m_dev_collection_cb_enabled = false;
|
||||
|
||||
// Cubeb callbacks
|
||||
static long data_cb(cubeb_stream* stream, void* user_ptr, void const* input_buffer, void* output_buffer, long nframes);
|
||||
static void state_cb(cubeb_stream* stream, void* user_ptr, cubeb_state state);
|
||||
static void device_collection_changed_cb(cubeb* context, void* user_ptr);
|
||||
|
||||
void CloseUnlocked();
|
||||
|
||||
std::tuple<cubeb_devid, std::string /* dev_ident */, u32 /* ch_cnt */> GetDevice(std::string_view dev_id = "");
|
||||
std::tuple<cubeb_devid, std::string /* dev_ident */, u32 /* ch_cnt */> GetDefaultDeviceAlt(AudioFreq freq, AudioSampleSize sample_size, AudioChannelCnt ch_cnt);
|
||||
};
|
||||
|
113
rpcs3/Emu/Audio/Cubeb/cubeb_enumerator.cpp
Normal file
113
rpcs3/Emu/Audio/Cubeb/cubeb_enumerator.cpp
Normal file
@ -0,0 +1,113 @@
|
||||
#include "Emu/Audio/Cubeb/cubeb_enumerator.h"
|
||||
#include "util/logs.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <Windows.h>
|
||||
#include <system_error>
|
||||
#endif
|
||||
|
||||
LOG_CHANNEL(cubeb_dev_enum);
|
||||
|
||||
cubeb_enumerator::cubeb_enumerator() : audio_device_enumerator()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
// Cubeb requires COM to be initialized on the thread calling cubeb_init on Windows
|
||||
HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
if (SUCCEEDED(hr))
|
||||
{
|
||||
com_init_success = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (int err = cubeb_init(&ctx, "RPCS3 device enumeration", nullptr))
|
||||
{
|
||||
cubeb_dev_enum.error("cubeb_init() failed: %i", err);
|
||||
ctx = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
cubeb_enumerator::~cubeb_enumerator()
|
||||
{
|
||||
if (ctx)
|
||||
{
|
||||
cubeb_destroy(ctx);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
if (com_init_success)
|
||||
{
|
||||
CoUninitialize();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
std::vector<audio_device_enumerator::audio_device> cubeb_enumerator::get_output_devices()
|
||||
{
|
||||
if (ctx == nullptr)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
cubeb_device_collection dev_collection{};
|
||||
if (int err = cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &dev_collection))
|
||||
{
|
||||
cubeb_dev_enum.error("cubeb_enumerate_devices() failed: %i", err);
|
||||
return {};
|
||||
}
|
||||
|
||||
if (dev_collection.count == 0)
|
||||
{
|
||||
cubeb_dev_enum.error("No output devices available");
|
||||
if (int err = cubeb_device_collection_destroy(ctx, &dev_collection))
|
||||
{
|
||||
cubeb_dev_enum.error("cubeb_device_collection_destroy() failed: %i", err);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<audio_device> device_list{};
|
||||
|
||||
for (u64 dev_idx = 0; dev_idx < dev_collection.count; dev_idx++)
|
||||
{
|
||||
const cubeb_device_info& dev_info = dev_collection.device[dev_idx];
|
||||
|
||||
if (dev_info.state == CUBEB_DEVICE_STATE_UNPLUGGED)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
audio_device dev =
|
||||
{
|
||||
.id = std::string{dev_info.device_id},
|
||||
.name = std::string{dev_info.friendly_name},
|
||||
.max_ch = dev_info.max_channels
|
||||
};
|
||||
|
||||
if (dev.id.empty())
|
||||
{
|
||||
cubeb_dev_enum.warning("Empty device id - skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dev.name.empty())
|
||||
{
|
||||
dev.name = dev.id;
|
||||
}
|
||||
|
||||
cubeb_dev_enum.notice("Found device: id=%s, name=%s, max_ch=%d", dev.id, dev.name, dev.max_ch);
|
||||
device_list.emplace_back(dev);
|
||||
}
|
||||
|
||||
if (int err = cubeb_device_collection_destroy(ctx, &dev_collection))
|
||||
{
|
||||
cubeb_dev_enum.error("cubeb_device_collection_destroy() failed: %i", err);
|
||||
}
|
||||
|
||||
std::sort(device_list.begin(), device_list.end(), [](audio_device_enumerator::audio_device a, audio_device_enumerator::audio_device b)
|
||||
{
|
||||
return a.name < b.name;
|
||||
});
|
||||
|
||||
return device_list;
|
||||
}
|
22
rpcs3/Emu/Audio/Cubeb/cubeb_enumerator.h
Normal file
22
rpcs3/Emu/Audio/Cubeb/cubeb_enumerator.h
Normal 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
|
||||
};
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
90
rpcs3/Emu/Audio/FAudio/faudio_enumerator.cpp
Normal file
90
rpcs3/Emu/Audio/FAudio/faudio_enumerator.cpp
Normal 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;
|
||||
}
|
22
rpcs3/Emu/Audio/FAudio/faudio_enumerator.h
Normal file
22
rpcs3/Emu/Audio/FAudio/faudio_enumerator.h
Normal 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{};
|
||||
};
|
@ -10,18 +10,15 @@ public:
|
||||
|
||||
std::string_view GetName() const override { return "Null"sv; }
|
||||
|
||||
bool Open(AudioFreq /* freq */, AudioSampleSize /* sample_size */, AudioChannelCnt /* ch_cnt */) override
|
||||
bool Open(std::string_view /* dev_id */, AudioFreq /* freq */, AudioSampleSize /* sample_size */, AudioChannelCnt /* ch_cnt */) override
|
||||
{
|
||||
Close();
|
||||
return true;
|
||||
}
|
||||
void Close() override { m_playing = false; }
|
||||
|
||||
void SetWriteCallback(std::function<u32(u32, void *)> /* cb */) override {};
|
||||
f64 GetCallbackFrameLen() override { return 0.01; };
|
||||
|
||||
void SetErrorCallback(std::function<void()> /* cb */) override {};
|
||||
|
||||
void Play() override { m_playing = true; }
|
||||
void Pause() override { m_playing = false; }
|
||||
bool IsPlaying() override { return m_playing; }
|
||||
|
13
rpcs3/Emu/Audio/Null/null_enumerator.h
Normal file
13
rpcs3/Emu/Audio/Null/null_enumerator.h
Normal 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 {}; }
|
||||
};
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
};
|
||||
|
153
rpcs3/Emu/Audio/XAudio2/xaudio2_enumerator.cpp
Normal file
153
rpcs3/Emu/Audio/XAudio2/xaudio2_enumerator.cpp
Normal 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;
|
||||
}
|
17
rpcs3/Emu/Audio/XAudio2/xaudio2_enumerator.h
Normal file
17
rpcs3/Emu/Audio/XAudio2/xaudio2_enumerator.h
Normal 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;
|
||||
};
|
26
rpcs3/Emu/Audio/audio_device_enumerator.h
Normal file
26
rpcs3/Emu/Audio/audio_device_enumerator.h
Normal 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;
|
||||
};
|
@ -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
|
@ -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;
|
||||
};
|
@ -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()
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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 };
|
||||
|
@ -53,9 +53,11 @@
|
||||
</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">
|
||||
|
@ -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>
|
@ -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" />
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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"}},
|
||||
|
@ -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
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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.");
|
||||
|
Loading…
Reference in New Issue
Block a user