diff --git a/src/core/settings.cpp b/src/core/settings.cpp index cb1cfd9a5..e6249198d 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -287,6 +287,7 @@ void Settings::Load(SettingsInterface& si) ParseAudioBackend(si.GetStringValue("Audio", "Backend", GetAudioBackendName(DEFAULT_AUDIO_BACKEND)).c_str()) .value_or(DEFAULT_AUDIO_BACKEND); audio_driver = si.GetStringValue("Audio", "Driver"); + audio_output_device = si.GetStringValue("Audio", "OutputDevice"); audio_stretch_mode = AudioStream::ParseStretchMode( si.GetStringValue("Audio", "StretchMode", AudioStream::GetStretchModeName(DEFAULT_AUDIO_STRETCH_MODE)).c_str()) @@ -500,6 +501,7 @@ void Settings::Save(SettingsInterface& si) const si.SetStringValue("Audio", "Backend", GetAudioBackendName(audio_backend)); si.SetStringValue("Audio", "Driver", audio_driver.c_str()); + si.SetStringValue("Audio", "OutputDevice", audio_output_device.c_str()); si.SetStringValue("Audio", "StretchMode", AudioStream::GetStretchModeName(audio_stretch_mode)); si.SetUIntValue("Audio", "BufferMS", audio_buffer_ms); si.SetUIntValue("Audio", "OutputLatencyMS", audio_output_latency_ms); diff --git a/src/core/settings.h b/src/core/settings.h index 11dfe63be..9e5305ab4 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -159,6 +159,7 @@ struct Settings AudioBackend audio_backend = DEFAULT_AUDIO_BACKEND; AudioStretchMode audio_stretch_mode = DEFAULT_AUDIO_STRETCH_MODE; std::string audio_driver; + std::string audio_output_device; u32 audio_output_latency_ms = DEFAULT_AUDIO_OUTPUT_LATENCY_MS; u32 audio_buffer_ms = DEFAULT_AUDIO_BUFFER_MS; u32 audio_output_volume = 100; diff --git a/src/core/system.cpp b/src/core/system.cpp index a3909bfbb..08d7b18fd 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -3128,7 +3128,9 @@ void System::CheckForSettingsChanges(const Settings& old_settings) UpdateOverclock(); } - if (g_settings.audio_backend != old_settings.audio_backend || g_settings.audio_driver != old_settings.audio_driver) + if (g_settings.audio_backend != old_settings.audio_backend || + g_settings.audio_driver != old_settings.audio_driver || + g_settings.audio_output_device != old_settings.audio_output_device) { if (g_settings.audio_backend != old_settings.audio_backend) { diff --git a/src/duckstation-qt/audiosettingswidget.cpp b/src/duckstation-qt/audiosettingswidget.cpp index 1d2ad7c89..ee4ffa4ad 100644 --- a/src/duckstation-qt/audiosettingswidget.cpp +++ b/src/duckstation-qt/audiosettingswidget.cpp @@ -102,26 +102,50 @@ void AudioSettingsWidget::updateDriverNames() .value_or(Settings::DEFAULT_AUDIO_BACKEND); std::vector names; + std::vector> devices; #ifdef WITH_CUBEB if (backend == AudioBackend::Cubeb) + { names = CommonHost::GetCubebDriverNames(); + devices = CommonHost::GetCubebOutputDevices(m_dialog->getEffectiveStringValue("Audio", "Driver", "").c_str()); + } #endif m_ui.driver->disconnect(); + m_ui.driver->clear(); if (names.empty()) { + m_ui.driver->addItem(tr("Default")); m_ui.driver->setEnabled(false); - m_ui.driver->clear(); - return; + } + else + { + m_ui.driver->setEnabled(true); + for (const std::string& name : names) + m_ui.driver->addItem(QString::fromStdString(name)); + + SettingWidgetBinder::BindWidgetToStringSetting(m_dialog->getSettingsInterface(), m_ui.driver, "Audio", "Driver", + std::move(names.front())); + connect(m_ui.driver, &QComboBox::currentIndexChanged, this, &AudioSettingsWidget::updateDriverNames); } - m_ui.driver->setEnabled(true); - for (const std::string& name : names) - m_ui.driver->addItem(QString::fromStdString(name)); + m_ui.outputDevice->disconnect(); + m_ui.outputDevice->clear(); + if (names.empty()) + { + m_ui.outputDevice->addItem(tr("Default")); + m_ui.outputDevice->setEnabled(false); + } + else + { + m_ui.outputDevice->setEnabled(true); + for (const auto& [id, name] : devices) + m_ui.outputDevice->addItem(QString::fromStdString(name), QString::fromStdString(id)); - SettingWidgetBinder::BindWidgetToStringSetting(m_dialog->getSettingsInterface(), m_ui.driver, "Audio", "Driver", - std::move(names.front())); + SettingWidgetBinder::BindWidgetToStringSetting(m_dialog->getSettingsInterface(), m_ui.outputDevice, "Audio", + "OutputDevice", std::move(devices.front().first)); + } } void AudioSettingsWidget::updateLatencyLabel() diff --git a/src/duckstation-qt/audiosettingswidget.ui b/src/duckstation-qt/audiosettingswidget.ui index 902627f5d..f5f92a456 100644 --- a/src/duckstation-qt/audiosettingswidget.ui +++ b/src/duckstation-qt/audiosettingswidget.ui @@ -32,111 +32,7 @@ Configuration - - - - Buffer Size: - - - - - - - Start Dumping On Boot - - - - - - - Backend: - - - - - - - - 500 - - - Qt::Horizontal - - - QSlider::TicksBothSides - - - 20 - - - - - - - Minimal - - - - - - - - - - - - - Off (Noisy) - - - - - Resampling (Pitch Shift) - - - - - Time Stretch (Tempo Change, Best Sound) - - - - - - - - Output Latency: - - - - - - - - - - Driver: - - - - - - - Stretch Mode: - - - - - - - Maximum latency: 0 frames (0.00ms) - - - Qt::AlignCenter - - - - 15 @@ -164,6 +60,120 @@ + + + + + + + Stretch Mode: + + + + + + + Buffer Size: + + + + + + + + Off (Noisy) + + + + + Resampling (Pitch Shift) + + + + + Time Stretch (Tempo Change, Best Sound) + + + + + + + + Maximum latency: 0 frames (0.00ms) + + + Qt::AlignCenter + + + + + + + Backend: + + + + + + + + + + Output Latency: + + + + + + + Driver: + + + + + + + Start Dumping On Boot + + + + + + + + + 500 + + + Qt::Horizontal + + + QSlider::TicksBothSides + + + 20 + + + + + + + Minimal + + + + + + + + + Output Device: + + + + + + diff --git a/src/frontend-common/common_host.h b/src/frontend-common/common_host.h index 0bc8cd8da..b8d342f71 100644 --- a/src/frontend-common/common_host.h +++ b/src/frontend-common/common_host.h @@ -6,6 +6,7 @@ #include #include #include +#include #include class SettingsInterface; @@ -42,6 +43,7 @@ u64 GetSessionPlayedTime(); std::unique_ptr CreateCubebAudioStream(u32 sample_rate, u32 channels, u32 buffer_ms, u32 latency_ms, AudioStretchMode stretch); std::vector GetCubebDriverNames(); +std::vector> GetCubebOutputDevices(const char* driver); #endif #ifdef _WIN32 std::unique_ptr CreateXAudio2Stream(u32 sample_rate, u32 channels, u32 buffer_ms, u32 latency_ms, diff --git a/src/frontend-common/cubeb_audio_stream.cpp b/src/frontend-common/cubeb_audio_stream.cpp index c4126621a..fb257df3c 100644 --- a/src/frontend-common/cubeb_audio_stream.cpp +++ b/src/frontend-common/cubeb_audio_stream.cpp @@ -4,11 +4,13 @@ #include "cubeb_audio_stream.h" #include "common/assert.h" #include "common/log.h" +#include "common/scoped_guard.h" #include "common/string_util.h" #include "common_host.h" #include "core/host.h" #include "core/settings.h" #include "cubeb/cubeb.h" +#include "fmt/format.h" Log_SetChannel(CubebAudioStream); #ifdef _WIN32 @@ -123,6 +125,40 @@ bool CubebAudioStream::Initialize(u32 latency_ms) } } + cubeb_devid selected_device = nullptr; + const std::string& selected_device_name = g_settings.audio_output_device; + cubeb_device_collection devices; + bool devices_valid = false; + if (!selected_device_name.empty()) + { + rv = cubeb_enumerate_devices(m_context, CUBEB_DEVICE_TYPE_OUTPUT, &devices); + devices_valid = (rv == CUBEB_OK); + if (rv == CUBEB_OK) + { + for (size_t i = 0; i < devices.count; i++) + { + const cubeb_device_info& di = devices.device[i]; + if (di.device_id && selected_device_name == di.device_id) + { + Log_InfoPrintf("Using output device '%s' (%s).", di.device_id, + di.friendly_name ? di.friendly_name : di.device_id); + selected_device = di.devid; + break; + } + } + + if (!selected_device) + { + Host::AddOSDMessage( + fmt::format("Requested audio output device '{}' not found, using default.", selected_device_name), 10.0f); + } + } + else + { + Log_WarningPrintf("cubeb_enumerate_devices() returned %d, using default device.", rv); + } + } + BaseInitialize(); m_volume = 100; m_paused = false; @@ -130,8 +166,12 @@ bool CubebAudioStream::Initialize(u32 latency_ms) char stream_name[32]; std::snprintf(stream_name, sizeof(stream_name), "%p", this); - rv = cubeb_stream_init(m_context, &stream, stream_name, nullptr, nullptr, nullptr, ¶ms, latency_frames, + rv = cubeb_stream_init(m_context, &stream, stream_name, nullptr, nullptr, selected_device, ¶ms, latency_frames, &CubebAudioStream::DataCallback, StateCallback, this); + + if (devices_valid) + cubeb_device_collection_destroy(m_context, &devices); + if (rv != CUBEB_OK) { Log_ErrorPrintf("(Cubeb) Could not create stream: %d", rv); @@ -210,3 +250,40 @@ std::vector CommonHost::GetCubebDriverNames() names.emplace_back(cubeb_names[i]); return names; } + +std::vector> CommonHost::GetCubebOutputDevices(const char* driver) +{ + std::vector> ret; + ret.emplace_back(std::string(), Host::TranslateStdString("CommonHost", "Default Output Device")); + + cubeb* context; + int rv = cubeb_init(&context, "DuckStation", (driver && *driver) ? driver : nullptr); + if (rv != CUBEB_OK) + { + Log_ErrorPrintf("cubeb_init() failed: %d", rv); + return ret; + } + + ScopedGuard context_cleanup([context]() { cubeb_destroy(context); }); + + cubeb_device_collection devices; + rv = cubeb_enumerate_devices(context, CUBEB_DEVICE_TYPE_OUTPUT, &devices); + if (rv != CUBEB_OK) + { + Log_ErrorPrintf("cubeb_enumerate_devices() failed: %d", rv); + return ret; + } + + ScopedGuard devices_cleanup([context, &devices]() { cubeb_device_collection_destroy(context, &devices); }); + + for (size_t i = 0; i < devices.count; i++) + { + const cubeb_device_info& di = devices.device[i]; + if (!di.device_id) + continue; + + ret.emplace_back(di.device_id, di.friendly_name ? di.friendly_name : di.device_id); + } + + return ret; +}