SPU2: Add output device selection

This commit is contained in:
Stenzek 2022-12-30 20:51:16 +10:00 committed by refractionpcsx2
parent 0c8beedb94
commit 9bca1946ed
10 changed files with 197 additions and 20 deletions

View File

@ -79,6 +79,7 @@ AudioSettingsWidget::AudioSettingsWidget(SettingsDialog* dialog, QWidget* parent
connect(m_ui.targetLatency, &QSlider::valueChanged, this, &AudioSettingsWidget::updateLatencyLabels); connect(m_ui.targetLatency, &QSlider::valueChanged, this, &AudioSettingsWidget::updateLatencyLabels);
connect(m_ui.outputLatency, &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::updateLatencyLabels);
connect(m_ui.outputLatencyMinimal, &QCheckBox::stateChanged, this, &AudioSettingsWidget::onMinimalOutputLatencyStateChanged);
outputModuleChanged(); outputModuleChanged();
m_ui.volume->setValue(m_dialog->getEffectiveIntValue("SPU2/Mixing", "FinalVolume", DEFAULT_VOLUME)); m_ui.volume->setValue(m_dialog->getEffectiveIntValue("SPU2/Mixing", "FinalVolume", DEFAULT_VOLUME));
@ -168,6 +169,8 @@ void AudioSettingsWidget::outputModuleChanged()
m_ui.backend->setCurrentIndex(index); m_ui.backend->setCurrentIndex(index);
} }
} }
updateDevices();
} }
void AudioSettingsWidget::outputBackendChanged() void AudioSettingsWidget::outputBackendChanged()
@ -188,6 +191,40 @@ void AudioSettingsWidget::outputBackendChanged()
m_dialog->setStringSettingValue("SPU2/Output", "BackendName", ""); m_dialog->setStringSettingValue("SPU2/Output", "BackendName", "");
else else
m_dialog->setStringSettingValue("SPU2/Output", "BackendName", m_ui.backend->currentText().toUtf8().constData()); 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<SndOutDeviceInfo> 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) 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.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())); 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<u32>(m_ui.outputLatency->value()); const u32 output_latency_ms =
minimal_output ? (((m_output_device_latency * 1000u) + 47999u) / 48000u) : static_cast<u32>(m_ui.outputLatency->value());
const u32 buffer_ms = static_cast<u32>(m_ui.targetLatency->value()); const u32 buffer_ms = static_cast<u32>(m_ui.targetLatency->value());
if (output_latency_ms > 0) if (output_latency_ms > 0)
{ {
@ -248,7 +286,7 @@ void AudioSettingsWidget::updateLatencyLabels()
} }
else 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));
} }
} }

View File

@ -33,6 +33,7 @@ private Q_SLOTS:
void expansionModeChanged(); void expansionModeChanged();
void outputModuleChanged(); void outputModuleChanged();
void outputBackendChanged(); void outputBackendChanged();
void updateDevices();
void volumeChanged(int value); void volumeChanged(int value);
void updateTargetLatencyRange(); void updateTargetLatencyRange();
void updateLatencyLabels(); void updateLatencyLabels();
@ -45,4 +46,5 @@ private Q_SLOTS:
private: private:
SettingsDialog* m_dialog; SettingsDialog* m_dialog;
Ui::AudioSettingsWidget m_ui; Ui::AudioSettingsWidget m_ui;
u32 m_output_device_latency = 0;
}; };

View File

@ -448,14 +448,14 @@
<item row="0" column="1"> <item row="0" column="1">
<widget class="QComboBox" name="outputModule"/> <widget class="QComboBox" name="outputModule"/>
</item> </item>
<item row="2" column="0"> <item row="3" column="0">
<widget class="QLabel" name="label_5"> <widget class="QLabel" name="label_5">
<property name="text"> <property name="text">
<string>Output Latency:</string> <string>Output Latency:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="3" column="1">
<layout class="QHBoxLayout" name="horizontalLayout"> <layout class="QHBoxLayout" name="horizontalLayout">
<item> <item>
<widget class="QSlider" name="outputLatency"> <widget class="QSlider" name="outputLatency">
@ -505,7 +505,7 @@
<item row="1" column="1"> <item row="1" column="1">
<widget class="QComboBox" name="backend"/> <widget class="QComboBox" name="backend"/>
</item> </item>
<item row="3" column="1"> <item row="4" column="1">
<widget class="QLabel" name="latencySummary"> <widget class="QLabel" name="latencySummary">
<property name="text"> <property name="text">
<string>Maximum Latency:</string> <string>Maximum Latency:</string>
@ -515,6 +515,16 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>Output Device:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="outputDevice"/>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>

View File

@ -842,6 +842,7 @@ struct Pcsx2Config
std::string OutputModule; std::string OutputModule;
std::string BackendName; std::string BackendName;
std::string DeviceName;
SPU2Options(); SPU2Options();
@ -865,7 +866,8 @@ struct Pcsx2Config
OpEqu(OverlapMS) && OpEqu(OverlapMS) &&
OpEqu(OutputModule) && OpEqu(OutputModule) &&
OpEqu(BackendName); OpEqu(BackendName) &&
OpEqu(DeviceName);
} }
bool operator!=(const SPU2Options& right) const bool operator!=(const SPU2Options& right) const

View File

@ -826,6 +826,7 @@ void Pcsx2Config::SPU2Options::LoadSave(SettingsWrapper& wrap)
SettingsWrapEntry(OutputModule); SettingsWrapEntry(OutputModule);
SettingsWrapEntry(BackendName); SettingsWrapEntry(BackendName);
SettingsWrapEntry(DeviceName);
SettingsWrapEntry(Latency); SettingsWrapEntry(Latency);
SettingsWrapEntry(OutputLatency); SettingsWrapEntry(OutputLatency);
SettingsWrapBitBool(OutputLatencyMinimal); SettingsWrapBitBool(OutputLatencyMinimal);

View File

@ -74,6 +74,11 @@ public:
{ {
return nullptr; return nullptr;
} }
std::vector<SndOutDeviceInfo> GetOutputDeviceList(const char* driver) const override
{
return {};
}
}; };
} }
@ -113,17 +118,27 @@ static SndOutModule* FindOutputModule(const char* name)
const char* const* GetOutputModuleBackends(const char* omodid) const char* const* GetOutputModuleBackends(const char* omodid)
{ {
for (SndOutModule* mod : mods) if (SndOutModule* mod = FindOutputModule(omodid))
{ return mod->GetBackendNames();
if (mod && std::strcmp(mod->GetIdent(), omodid) == 0)
{
return mod->GetBackendNames();
}
}
return nullptr; 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<SndOutDeviceInfo> GetOutputDeviceList(const char* omodid, const char* driver)
{
std::vector<SndOutDeviceInfo> ret;
if (SndOutModule* mod = FindOutputModule(omodid))
ret = mod->GetOutputDeviceList(driver);
return ret;
}
StereoOut32* SndBuffer::m_buffer; StereoOut32* SndBuffer::m_buffer;
s32 SndBuffer::m_size; s32 SndBuffer::m_size;
alignas(4) volatile s32 SndBuffer::m_rpos; alignas(4) volatile s32 SndBuffer::m_rpos;

View File

@ -15,6 +15,8 @@
#pragma once #pragma once
#include <vector>
// Number of stereo samples per SndOut block. // Number of stereo samples per SndOut block.
// All drivers must work in units of this size when communicating with // All drivers must work in units of this size when communicating with
// SndOut. // SndOut.
@ -36,6 +38,18 @@ extern int SampleRate;
// nullptr is returned if the specified module does not have multiple backends. // nullptr is returned if the specified module does not have multiple backends.
extern const char* const* GetOutputModuleBackends(const char* omodid); 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<SndOutDeviceInfo> GetOutputDeviceList(const char* omodid, const char* driver);
struct Stereo51Out16DplII; struct Stereo51Out16DplII;
struct Stereo51Out32DplII; struct Stereo51Out32DplII;
@ -460,6 +474,9 @@ public:
// Returns a null-terminated list of backends, or nullptr. // Returns a null-terminated list of backends, or nullptr.
virtual const char* const* GetBackendNames() const = 0; virtual const char* const* GetBackendNames() const = 0;
// Returns a list of output devices and their associated minimum latency.
virtual std::vector<SndOutDeviceInfo> GetOutputDeviceList(const char* driver) const = 0;
virtual bool Init() = 0; virtual bool Init() = 0;
virtual void Close() = 0; virtual void Close() = 0;

View File

@ -18,10 +18,12 @@
#include "SPU2/Global.h" #include "SPU2/Global.h"
#include "SPU2/SndOut.h" #include "SPU2/SndOut.h"
#include "Host.h" #include "Host.h"
#include "IconsFontAwesome5.h"
#include "common/Console.h" #include "common/Console.h"
#include "common/StringUtil.h" #include "common/StringUtil.h"
#include "common/RedtapeWindows.h" #include "common/RedtapeWindows.h"
#include "common/ScopedGuard.h"
#include "cubeb/cubeb.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]; char stream_name[32];
std::snprintf(stream_name, sizeof(stream_name), "%p", this); std::snprintf(stream_name, sizeof(stream_name), "%p", this);
rv = cubeb_stream_init(m_context, &stream, stream_name, nullptr, nullptr, nullptr, &params, rv = cubeb_stream_init(m_context, &stream, stream_name, nullptr, nullptr, selected_device, &params,
latency_frames, &Cubeb::DataCallback, &Cubeb::StateCallback, this); latency_frames, &Cubeb::DataCallback, &Cubeb::StateCallback, this);
if (rv != CUBEB_OK) if (rv != CUBEB_OK)
{ {
@ -353,6 +389,55 @@ public:
{ {
return cubeb_get_backend_names(); return cubeb_get_backend_names();
} }
std::vector<SndOutDeviceInfo> GetOutputDeviceList(const char* driver) const override
{
std::vector<SndOutDeviceInfo> 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, &params, &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; static Cubeb s_Cubeb;

View File

@ -369,6 +369,10 @@ public:
return nullptr; return nullptr;
} }
std::vector<SndOutDeviceInfo> GetOutputDeviceList(const char* driver) const override
{
return {};
}
} static XA2; } static XA2;
SndOutModule* XAudio2Out = &XA2; SndOutModule* XAudio2Out = &XA2;

View File

@ -21,11 +21,11 @@
namespace SPU2 namespace SPU2
{ {
static int GetConsoleSampleRate(); static int GetConsoleSampleRate();
static void InitSndBuffer(); static void InitSndBuffer();
static void UpdateSampleRate(); static void UpdateSampleRate();
static void InternalReset(bool psxmode); static void InternalReset(bool psxmode);
} } // namespace SPU2
static double s_device_sample_rate_multiplier = 1.0; static double s_device_sample_rate_multiplier = 1.0;
static bool s_psxmode = false; 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. // Wipe buffer out when changing sync mode, so e.g. TS->none doesn't have a huge delay.
if (opts.SynchMode != oldopts.SynchMode) if (opts.SynchMode != oldopts.SynchMode)
SndBuffer::ResetBuffers(); SndBuffer::ResetBuffers();
// Things which require re-initialzing the output. // Things which require re-initialzing the output.
if (opts.Latency != oldopts.Latency || if (opts.Latency != oldopts.Latency ||
opts.OutputLatency != oldopts.OutputLatency || opts.OutputLatency != oldopts.OutputLatency ||
opts.OutputLatencyMinimal != oldopts.OutputLatencyMinimal || opts.OutputLatencyMinimal != oldopts.OutputLatencyMinimal ||
opts.OutputModule != oldopts.OutputModule ||
opts.BackendName != oldopts.BackendName ||
opts.DeviceName != oldopts.DeviceName ||
opts.SpeakerConfiguration != oldopts.SpeakerConfiguration || opts.SpeakerConfiguration != oldopts.SpeakerConfiguration ||
opts.DplDecodingLevel != oldopts.DplDecodingLevel || opts.DplDecodingLevel != oldopts.DplDecodingLevel ||
opts.SequenceLenMS != oldopts.SequenceLenMS || opts.SequenceLenMS != oldopts.SequenceLenMS ||