diff --git a/pcsx2-qt/Settings/AudioSettingsWidget.cpp b/pcsx2-qt/Settings/AudioSettingsWidget.cpp index 023f0fef06..f48df33d38 100644 --- a/pcsx2-qt/Settings/AudioSettingsWidget.cpp +++ b/pcsx2-qt/Settings/AudioSettingsWidget.cpp @@ -79,6 +79,7 @@ AudioSettingsWidget::AudioSettingsWidget(SettingsDialog* dialog, QWidget* parent connect(m_ui.targetLatency, &QSlider::valueChanged, this, &AudioSettingsWidget::updateLatencyLabels); connect(m_ui.outputLatency, &QSlider::valueChanged, this, &AudioSettingsWidget::updateLatencyLabels); connect(m_ui.outputLatencyMinimal, &QCheckBox::stateChanged, this, &AudioSettingsWidget::updateLatencyLabels); + connect(m_ui.outputLatencyMinimal, &QCheckBox::stateChanged, this, &AudioSettingsWidget::onMinimalOutputLatencyStateChanged); outputModuleChanged(); m_ui.volume->setValue(m_dialog->getEffectiveIntValue("SPU2/Mixing", "FinalVolume", DEFAULT_VOLUME)); @@ -168,6 +169,8 @@ void AudioSettingsWidget::outputModuleChanged() m_ui.backend->setCurrentIndex(index); } } + + updateDevices(); } void AudioSettingsWidget::outputBackendChanged() @@ -188,6 +191,40 @@ void AudioSettingsWidget::outputBackendChanged() m_dialog->setStringSettingValue("SPU2/Output", "BackendName", ""); else m_dialog->setStringSettingValue("SPU2/Output", "BackendName", m_ui.backend->currentText().toUtf8().constData()); + + updateDevices(); +} + +void AudioSettingsWidget::updateDevices() +{ + const std::string module_name(m_dialog->getEffectiveStringValue("SPU2/Output", "OutputModule", DEFAULT_OUTPUT_MODULE)); + const std::string backend_name(m_dialog->getEffectiveStringValue("SPU2/Output", "BackendName", "")); + + m_ui.outputDevice->disconnect(); + m_ui.outputDevice->clear(); + m_output_device_latency = 0; + + std::vector devices(GetOutputDeviceList(module_name.c_str(), backend_name.c_str())); + if (devices.empty()) + { + m_ui.outputDevice->addItem(tr("Default")); + m_ui.outputDevice->setEnabled(false); + } + else + { + const std::string current_device(m_dialog->getEffectiveStringValue("SPU2/Output", "DeviceName", "")); + + m_ui.outputDevice->setEnabled(true); + for (const SndOutDeviceInfo& devi : devices) + { + m_ui.outputDevice->addItem(QString::fromStdString(devi.display_name), QString::fromStdString(devi.name)); + if (devi.name == current_device) + m_output_device_latency = devi.minimum_latency_frames; + } + + SettingWidgetBinder::BindWidgetToStringSetting( + m_dialog->getSettingsInterface(), m_ui.outputDevice, "SPU2/Output", "DeviceName", std::move(devices.front().name)); + } } void AudioSettingsWidget::volumeChanged(int value) @@ -237,7 +274,8 @@ void AudioSettingsWidget::updateLatencyLabels() m_ui.targetLatencyLabel->setText(tr("%1 ms").arg(m_ui.targetLatency->value())); m_ui.outputLatencyLabel->setText(minimal_output ? tr("N/A") : tr("%1 ms").arg(m_ui.outputLatency->value())); - const u32 output_latency_ms = minimal_output ? 0 : static_cast(m_ui.outputLatency->value()); + const u32 output_latency_ms = + minimal_output ? (((m_output_device_latency * 1000u) + 47999u) / 48000u) : static_cast(m_ui.outputLatency->value()); const u32 buffer_ms = static_cast(m_ui.targetLatency->value()); if (output_latency_ms > 0) { @@ -248,7 +286,7 @@ void AudioSettingsWidget::updateLatencyLabels() } else { - m_ui.latencySummary->setText(tr("Average Latency: %1 ms (plus minimum output)").arg(buffer_ms)); + m_ui.latencySummary->setText(tr("Average Latency: %1 ms (minimum output latency unknown)").arg(buffer_ms)); } } diff --git a/pcsx2-qt/Settings/AudioSettingsWidget.h b/pcsx2-qt/Settings/AudioSettingsWidget.h index 5b12c8e5a5..c81e25fdfd 100644 --- a/pcsx2-qt/Settings/AudioSettingsWidget.h +++ b/pcsx2-qt/Settings/AudioSettingsWidget.h @@ -33,6 +33,7 @@ private Q_SLOTS: void expansionModeChanged(); void outputModuleChanged(); void outputBackendChanged(); + void updateDevices(); void volumeChanged(int value); void updateTargetLatencyRange(); void updateLatencyLabels(); @@ -45,4 +46,5 @@ private Q_SLOTS: private: SettingsDialog* m_dialog; Ui::AudioSettingsWidget m_ui; + u32 m_output_device_latency = 0; }; diff --git a/pcsx2-qt/Settings/AudioSettingsWidget.ui b/pcsx2-qt/Settings/AudioSettingsWidget.ui index e3e9a99d0e..1f5bb78c5d 100644 --- a/pcsx2-qt/Settings/AudioSettingsWidget.ui +++ b/pcsx2-qt/Settings/AudioSettingsWidget.ui @@ -448,14 +448,14 @@ - + Output Latency: - + @@ -505,7 +505,7 @@ - + Maximum Latency: @@ -515,6 +515,16 @@ + + + + Output Device: + + + + + + diff --git a/pcsx2/Config.h b/pcsx2/Config.h index e0c38ad4a9..1fa2bf3851 100644 --- a/pcsx2/Config.h +++ b/pcsx2/Config.h @@ -842,6 +842,7 @@ struct Pcsx2Config std::string OutputModule; std::string BackendName; + std::string DeviceName; SPU2Options(); @@ -865,7 +866,8 @@ struct Pcsx2Config OpEqu(OverlapMS) && OpEqu(OutputModule) && - OpEqu(BackendName); + OpEqu(BackendName) && + OpEqu(DeviceName); } bool operator!=(const SPU2Options& right) const diff --git a/pcsx2/Pcsx2Config.cpp b/pcsx2/Pcsx2Config.cpp index fbc99546f9..853083fcd0 100644 --- a/pcsx2/Pcsx2Config.cpp +++ b/pcsx2/Pcsx2Config.cpp @@ -826,6 +826,7 @@ void Pcsx2Config::SPU2Options::LoadSave(SettingsWrapper& wrap) SettingsWrapEntry(OutputModule); SettingsWrapEntry(BackendName); + SettingsWrapEntry(DeviceName); SettingsWrapEntry(Latency); SettingsWrapEntry(OutputLatency); SettingsWrapBitBool(OutputLatencyMinimal); diff --git a/pcsx2/SPU2/SndOut.cpp b/pcsx2/SPU2/SndOut.cpp index 15dfce443d..38d13abf47 100644 --- a/pcsx2/SPU2/SndOut.cpp +++ b/pcsx2/SPU2/SndOut.cpp @@ -74,6 +74,11 @@ public: { return nullptr; } + + std::vector GetOutputDeviceList(const char* driver) const override + { + return {}; + } }; } @@ -113,17 +118,27 @@ static SndOutModule* FindOutputModule(const char* name) const char* const* GetOutputModuleBackends(const char* omodid) { - for (SndOutModule* mod : mods) - { - if (mod && std::strcmp(mod->GetIdent(), omodid) == 0) - { - return mod->GetBackendNames(); - } - } + if (SndOutModule* mod = FindOutputModule(omodid)) + return mod->GetBackendNames(); return nullptr; } +SndOutDeviceInfo::SndOutDeviceInfo(std::string name_, std::string display_name_, u32 minimum_latency_) + : name(std::move(name_)), display_name(std::move(display_name_)), minimum_latency_frames(minimum_latency_) +{ +} + +SndOutDeviceInfo::~SndOutDeviceInfo() = default; + +std::vector GetOutputDeviceList(const char* omodid, const char* driver) +{ + std::vector ret; + if (SndOutModule* mod = FindOutputModule(omodid)) + ret = mod->GetOutputDeviceList(driver); + return ret; +} + StereoOut32* SndBuffer::m_buffer; s32 SndBuffer::m_size; alignas(4) volatile s32 SndBuffer::m_rpos; diff --git a/pcsx2/SPU2/SndOut.h b/pcsx2/SPU2/SndOut.h index d7791eb191..1a2a1b454b 100644 --- a/pcsx2/SPU2/SndOut.h +++ b/pcsx2/SPU2/SndOut.h @@ -15,6 +15,8 @@ #pragma once +#include + // Number of stereo samples per SndOut block. // All drivers must work in units of this size when communicating with // SndOut. @@ -36,6 +38,18 @@ extern int SampleRate; // nullptr is returned if the specified module does not have multiple backends. extern const char* const* GetOutputModuleBackends(const char* omodid); +// Returns a list of output devices and their associated minimum latency. +struct SndOutDeviceInfo +{ + std::string name; + std::string display_name; + u32 minimum_latency_frames; + + SndOutDeviceInfo(std::string name_, std::string display_name_, u32 minimum_latency_); + ~SndOutDeviceInfo(); +}; +std::vector GetOutputDeviceList(const char* omodid, const char* driver); + struct Stereo51Out16DplII; struct Stereo51Out32DplII; @@ -460,6 +474,9 @@ public: // Returns a null-terminated list of backends, or nullptr. virtual const char* const* GetBackendNames() const = 0; + // Returns a list of output devices and their associated minimum latency. + virtual std::vector GetOutputDeviceList(const char* driver) const = 0; + virtual bool Init() = 0; virtual void Close() = 0; diff --git a/pcsx2/SPU2/SndOut_Cubeb.cpp b/pcsx2/SPU2/SndOut_Cubeb.cpp index b084759bea..7c2adaae6e 100644 --- a/pcsx2/SPU2/SndOut_Cubeb.cpp +++ b/pcsx2/SPU2/SndOut_Cubeb.cpp @@ -18,10 +18,12 @@ #include "SPU2/Global.h" #include "SPU2/SndOut.h" #include "Host.h" +#include "IconsFontAwesome5.h" #include "common/Console.h" #include "common/StringUtil.h" #include "common/RedtapeWindows.h" +#include "common/ScopedGuard.h" #include "cubeb/cubeb.h" @@ -273,10 +275,44 @@ public: } } + cubeb_devid selected_device = nullptr; + const std::string& selected_device_name = EmuConfig.SPU2.DeviceName; + 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) + { + Console.WriteLn("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::AddIconOSDMessage("CubebDeviceNotFound", ICON_FA_VOLUME_MUTE, + fmt::format("Requested audio output device '{}' not found, using default.", selected_device_name), + Host::OSD_WARNING_DURATION); + } + } + else + { + Console.Error("cubeb_enumerate_devices() returned %d, using default device.", rv); + } + } + 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, + rv = cubeb_stream_init(m_context, &stream, stream_name, nullptr, nullptr, selected_device, ¶ms, latency_frames, &Cubeb::DataCallback, &Cubeb::StateCallback, this); if (rv != CUBEB_OK) { @@ -353,6 +389,55 @@ public: { return cubeb_get_backend_names(); } + + std::vector GetOutputDeviceList(const char* driver) const override + { + std::vector ret; + ret.emplace_back(std::string(), "Default", 0u); + + cubeb* context; + int rv = cubeb_init(&context, "PCSX2", (driver && *driver) ? driver : nullptr); + if (rv != CUBEB_OK) + { + Console.Error("(GetOutputDeviceList) 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) + { + Console.Error("(GetOutputDeviceList) cubeb_enumerate_devices() failed: %d", rv); + return ret; + } + + ScopedGuard devices_cleanup([context, &devices]() { cubeb_device_collection_destroy(context, &devices); }); + + // we need stream parameters to query latency + cubeb_stream_params params = {}; + params.format = CUBEB_SAMPLE_S16LE; + params.rate = SampleRate; + params.channels = 2; + params.layout = CUBEB_LAYOUT_UNDEFINED; + params.prefs = CUBEB_STREAM_PREF_NONE; + + u32 min_latency = 0; + cubeb_get_min_latency(context, ¶ms, &min_latency); + ret[0].minimum_latency_frames = min_latency; + + 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, min_latency); + } + + return ret; + } }; static Cubeb s_Cubeb; diff --git a/pcsx2/SPU2/SndOut_XAudio2.cpp b/pcsx2/SPU2/SndOut_XAudio2.cpp index eb9bf56961..50ed0d11c5 100644 --- a/pcsx2/SPU2/SndOut_XAudio2.cpp +++ b/pcsx2/SPU2/SndOut_XAudio2.cpp @@ -369,6 +369,10 @@ public: return nullptr; } + std::vector GetOutputDeviceList(const char* driver) const override + { + return {}; + } } static XA2; SndOutModule* XAudio2Out = &XA2; diff --git a/pcsx2/SPU2/spu2.cpp b/pcsx2/SPU2/spu2.cpp index 566c1febda..9113f1e84d 100644 --- a/pcsx2/SPU2/spu2.cpp +++ b/pcsx2/SPU2/spu2.cpp @@ -21,11 +21,11 @@ namespace SPU2 { -static int GetConsoleSampleRate(); -static void InitSndBuffer(); -static void UpdateSampleRate(); -static void InternalReset(bool psxmode); -} + static int GetConsoleSampleRate(); + static void InitSndBuffer(); + static void UpdateSampleRate(); + static void InternalReset(bool psxmode); +} // namespace SPU2 static double s_device_sample_rate_multiplier = 1.0; static bool s_psxmode = false; @@ -392,11 +392,14 @@ void SPU2::CheckForConfigChanges(const Pcsx2Config& old_config) // Wipe buffer out when changing sync mode, so e.g. TS->none doesn't have a huge delay. if (opts.SynchMode != oldopts.SynchMode) SndBuffer::ResetBuffers(); - + // Things which require re-initialzing the output. if (opts.Latency != oldopts.Latency || opts.OutputLatency != oldopts.OutputLatency || opts.OutputLatencyMinimal != oldopts.OutputLatencyMinimal || + opts.OutputModule != oldopts.OutputModule || + opts.BackendName != oldopts.BackendName || + opts.DeviceName != oldopts.DeviceName || opts.SpeakerConfiguration != oldopts.SpeakerConfiguration || opts.DplDecodingLevel != oldopts.DplDecodingLevel || opts.SequenceLenMS != oldopts.SequenceLenMS ||