Path: blob/master/src/duckstation-qt/audiosettingswidget.cpp
4242 views
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <[email protected]>1// SPDX-License-Identifier: CC-BY-NC-ND-4.023#include "audiosettingswidget.h"4#include "qtutils.h"5#include "settingswindow.h"6#include "settingwidgetbinder.h"7#include "ui_audiostretchsettingsdialog.h"89#include "core/spu.h"1011#include "util/audio_stream.h"1213#include <bit>14#include <cmath>1516#include "moc_audiosettingswidget.cpp"1718AudioSettingsWidget::AudioSettingsWidget(SettingsWindow* dialog, QWidget* parent) : QWidget(parent), m_dialog(dialog)19{20SettingsInterface* sif = dialog->getSettingsInterface();2122m_ui.setupUi(this);2324SettingWidgetBinder::BindWidgetToEnumSetting(25sif, m_ui.audioBackend, "Audio", "Backend", &AudioStream::ParseBackendName, &AudioStream::GetBackendName,26&AudioStream::GetBackendDisplayName, AudioStream::DEFAULT_BACKEND, AudioBackend::Count);27SettingWidgetBinder::BindWidgetToEnumSetting(28sif, m_ui.stretchMode, "Audio", "StretchMode", &AudioStream::ParseStretchMode, &AudioStream::GetStretchModeName,29&AudioStream::GetStretchModeDisplayName, AudioStreamParameters::DEFAULT_STRETCH_MODE, AudioStretchMode::Count);30SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.bufferMS, "Audio", "BufferMS",31AudioStreamParameters::DEFAULT_BUFFER_MS);32SettingWidgetBinder::BindWidgetToIntSetting(sif, m_ui.outputLatencyMS, "Audio", "OutputLatencyMS",33AudioStreamParameters::DEFAULT_OUTPUT_LATENCY_MS);34SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.outputLatencyMinimal, "Audio", "OutputLatencyMinimal",35AudioStreamParameters::DEFAULT_OUTPUT_LATENCY_MINIMAL);36SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.muteCDAudio, "CDROM", "MuteCDAudio", false);37connect(m_ui.audioBackend, &QComboBox::currentIndexChanged, this, &AudioSettingsWidget::updateDriverNames);38connect(m_ui.stretchMode, &QComboBox::currentIndexChanged, this, &AudioSettingsWidget::onStretchModeChanged);39connect(m_ui.stretchSettings, &QToolButton::clicked, this, &AudioSettingsWidget::onStretchSettingsClicked);40onStretchModeChanged();41updateDriverNames();4243connect(m_ui.bufferMS, &QSlider::valueChanged, this, &AudioSettingsWidget::updateLatencyLabel);44connect(m_ui.outputLatencyMS, &QSlider::valueChanged, this, &AudioSettingsWidget::updateLatencyLabel);45connect(m_ui.outputLatencyMinimal, &QCheckBox::checkStateChanged, this,46&AudioSettingsWidget::onMinimalOutputLatencyChecked);47updateLatencyLabel();4849// for per-game, just use the normal path, since it needs to re-read/apply50if (!dialog->isPerGameSettings())51{52m_ui.volume->setValue(m_dialog->getEffectiveIntValue("Audio", "OutputVolume", 100));53m_ui.fastForwardVolume->setValue(m_dialog->getEffectiveIntValue("Audio", "FastForwardVolume", 100));54m_ui.muted->setChecked(m_dialog->getEffectiveBoolValue("Audio", "OutputMuted", false));55connect(m_ui.volume, &QSlider::valueChanged, this, &AudioSettingsWidget::onOutputVolumeChanged);56connect(m_ui.fastForwardVolume, &QSlider::valueChanged, this, &AudioSettingsWidget::onFastForwardVolumeChanged);57connect(m_ui.muted, &QCheckBox::checkStateChanged, this, &AudioSettingsWidget::onOutputMutedChanged);58updateVolumeLabel();59}60else61{62SettingWidgetBinder::BindWidgetAndLabelToIntSetting(sif, m_ui.volume, m_ui.volumeLabel, tr("%"), "Audio",63"OutputVolume", 100);64SettingWidgetBinder::BindWidgetAndLabelToIntSetting(sif, m_ui.fastForwardVolume, m_ui.fastForwardVolumeLabel,65tr("%"), "Audio", "FastForwardVolume", 100);66SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.muted, "Audio", "OutputMuted", false);67}68connect(m_ui.resetVolume, &QToolButton::clicked, this, [this]() { resetVolume(false); });69connect(m_ui.resetFastForwardVolume, &QToolButton::clicked, this, [this]() { resetVolume(true); });7071dialog->registerWidgetHelp(72m_ui.audioBackend, tr("Audio Backend"), QStringLiteral("Cubeb"),73tr("The audio backend determines how frames produced by the emulator are submitted to the host. Cubeb provides the "74"lowest latency, if you encounter issues, try the SDL backend. The null backend disables all host audio "75"output."));76dialog->registerWidgetHelp(77m_ui.bufferMS, tr("Buffer Size"), tr("%1 ms").arg(AudioStreamParameters::DEFAULT_BUFFER_MS),78tr("The buffer size determines the size of the chunks of audio which will be pulled by the "79"host. Smaller values reduce the output latency, but may cause hitches if the emulation "80"speed is inconsistent. Note that the Cubeb backend uses smaller chunks regardless of "81"this value, so using a low value here may not significantly change latency."));82dialog->registerWidgetHelp(83m_ui.outputLatencyMS, tr("Output Latency"), tr("%1 ms").arg(AudioStreamParameters::DEFAULT_OUTPUT_LATENCY_MS),84tr("Determines how much latency there is between the audio being picked up by the host API, and "85"played through speakers."));86dialog->registerWidgetHelp(m_ui.outputLatencyMinimal, tr("Minimal Output Latency"), tr("Unchecked"),87tr("When enabled, the minimum supported output latency will be used for the host API."));88dialog->registerWidgetHelp(m_ui.volume, tr("Output Volume"), "100%",89tr("Controls the volume of the audio played on the host."));90dialog->registerWidgetHelp(m_ui.fastForwardVolume, tr("Fast Forward Volume"), "100%",91tr("Controls the volume of the audio played on the host when fast forwarding."));92dialog->registerWidgetHelp(m_ui.muted, tr("Mute All Sound"), tr("Unchecked"),93tr("Prevents the emulator from producing any audible sound."));94dialog->registerWidgetHelp(m_ui.muteCDAudio, tr("Mute CD Audio"), tr("Unchecked"),95tr("Forcibly mutes both CD-DA and XA audio from the CD-ROM. Can be used to disable "96"background music in some games."));97dialog->registerWidgetHelp(98m_ui.stretchMode, tr("Stretch Mode"), tr("Time Stretching"),99tr("When running outside of 100% speed, adjusts the tempo on audio instead of dropping frames. Produces "100"much nicer fast forward/slowdown audio at a small cost to performance."));101dialog->registerWidgetHelp(m_ui.stretchSettings, tr("Stretch Settings"), tr("N/A"),102tr("These settings fine-tune the behavior of the SoundTouch audio time stretcher when "103"running outside of 100% speed."));104dialog->registerWidgetHelp(m_ui.resetVolume, tr("Reset Volume"), tr("N/A"),105m_dialog->isPerGameSettings() ? tr("Resets volume back to the global/inherited setting.") :106tr("Resets volume back to the default, i.e. full."));107dialog->registerWidgetHelp(m_ui.resetFastForwardVolume, tr("Reset Fast Forward Volume"), tr("N/A"),108m_dialog->isPerGameSettings() ? tr("Resets volume back to the global/inherited setting.") :109tr("Resets volume back to the default, i.e. full."));110}111112AudioSettingsWidget::~AudioSettingsWidget() = default;113114void AudioSettingsWidget::onStretchModeChanged()115{116const AudioStretchMode stretch_mode =117AudioStream::ParseStretchMode(118m_dialog119->getEffectiveStringValue("Audio", "StretchMode",120AudioStream::GetStretchModeName(AudioStreamParameters::DEFAULT_STRETCH_MODE))121.c_str())122.value_or(AudioStreamParameters::DEFAULT_STRETCH_MODE);123m_ui.stretchSettings->setEnabled(stretch_mode != AudioStretchMode::Off);124}125126AudioBackend AudioSettingsWidget::getEffectiveBackend() const127{128return AudioStream::ParseBackendName(129m_dialog130->getEffectiveStringValue("Audio", "Backend", AudioStream::GetBackendName(AudioStream::DEFAULT_BACKEND))131.c_str())132.value_or(AudioStream::DEFAULT_BACKEND);133}134135void AudioSettingsWidget::updateDriverNames()136{137const AudioBackend backend = getEffectiveBackend();138std::vector<std::pair<std::string, std::string>> names = AudioStream::GetDriverNames(backend);139140m_ui.driver->disconnect();141m_ui.driver->clear();142if (names.empty())143{144m_ui.driver->addItem(tr("Default"));145m_ui.driver->setEnabled(false);146}147else148{149m_ui.driver->setEnabled(true);150for (const auto& [name, display_name] : names)151m_ui.driver->addItem(QString::fromStdString(display_name), QString::fromStdString(name));152153SettingWidgetBinder::BindWidgetToStringSetting(m_dialog->getSettingsInterface(), m_ui.driver, "Audio", "Driver",154std::move(names.front().first));155connect(m_ui.driver, &QComboBox::currentIndexChanged, this, &AudioSettingsWidget::updateDeviceNames);156}157158updateDeviceNames();159}160161void AudioSettingsWidget::updateDeviceNames()162{163const AudioBackend backend = getEffectiveBackend();164const std::string driver_name = m_dialog->getEffectiveStringValue("Audio", "Driver", "");165const std::string current_device = m_dialog->getEffectiveStringValue("Audio", "Device", "");166std::vector<AudioStream::DeviceInfo> devices =167AudioStream::GetOutputDevices(backend, driver_name.c_str(), SPU::SAMPLE_RATE);168169SettingWidgetBinder::DisconnectWidget(m_ui.outputDevice);170m_ui.outputDevice->clear();171m_output_device_latency = 0;172173if (devices.empty())174{175m_ui.outputDevice->addItem(tr("Default"));176m_ui.outputDevice->setEnabled(false);177}178else179{180m_ui.outputDevice->setEnabled(true);181182bool is_known_device = false;183for (const AudioStream::DeviceInfo& di : devices)184{185m_ui.outputDevice->addItem(QString::fromStdString(di.display_name), QString::fromStdString(di.name));186if (di.name == current_device)187{188m_output_device_latency = di.minimum_latency_frames;189is_known_device = true;190}191}192193if (!is_known_device)194{195m_ui.outputDevice->addItem(tr("Unknown Device \"%1\"").arg(QString::fromStdString(current_device)),196QString::fromStdString(current_device));197}198199SettingWidgetBinder::BindWidgetToStringSetting(m_dialog->getSettingsInterface(), m_ui.outputDevice, "Audio",200"OutputDevice", std::move(devices.front().name));201}202203updateLatencyLabel();204}205206void AudioSettingsWidget::updateLatencyLabel()207{208const u32 config_buffer_ms =209m_dialog->getEffectiveIntValue("Audio", "BufferMS", AudioStreamParameters::DEFAULT_BUFFER_MS);210const u32 config_output_latency_ms =211m_dialog->getEffectiveIntValue("Audio", "OutputLatencyMS", AudioStreamParameters::DEFAULT_OUTPUT_LATENCY_MS);212const bool minimal_output = m_dialog->getEffectiveBoolValue("Audio", "OutputLatencyMinimal",213AudioStreamParameters::DEFAULT_OUTPUT_LATENCY_MINIMAL);214215//: Preserve the %1 variable, adapt the latter ms (and/or any possible spaces in between) to your language's ruleset.216m_ui.outputLatencyLabel->setText(minimal_output ? tr("N/A") : tr("%1 ms").arg(config_output_latency_ms));217m_ui.bufferMSLabel->setText(tr("%1 ms").arg(config_buffer_ms));218219const u32 output_latency_ms = minimal_output ?220AudioStream::GetMSForBufferSize(SPU::SAMPLE_RATE, m_output_device_latency) :221config_output_latency_ms;222if (output_latency_ms > 0)223{224m_ui.bufferingLabel->setText(tr("Maximum Latency: %1 ms (%2 ms buffer + %3 ms output)")225.arg(config_buffer_ms + output_latency_ms)226.arg(config_buffer_ms)227.arg(output_latency_ms));228}229else230{231m_ui.bufferingLabel->setText(tr("Maximum Latency: %1 ms (minimum output latency unknown)").arg(config_buffer_ms));232}233}234235void AudioSettingsWidget::updateVolumeLabel()236{237m_ui.volumeLabel->setText(tr("%1%").arg(m_ui.volume->value()));238m_ui.fastForwardVolumeLabel->setText(tr("%1%").arg(m_ui.fastForwardVolume->value()));239}240241void AudioSettingsWidget::onMinimalOutputLatencyChecked(Qt::CheckState state)242{243const bool minimal = m_dialog->getEffectiveBoolValue("Audio", "OutputLatencyMinimal", false);244m_ui.outputLatencyMS->setEnabled(!minimal);245updateLatencyLabel();246}247248void AudioSettingsWidget::onOutputVolumeChanged(int new_value)249{250// only called for base settings251DebugAssert(!m_dialog->isPerGameSettings());252Host::SetBaseIntSettingValue("Audio", "OutputVolume", new_value);253Host::CommitBaseSettingChanges();254g_emu_thread->setAudioOutputVolume(new_value, m_ui.fastForwardVolume->value());255256updateVolumeLabel();257}258259void AudioSettingsWidget::onFastForwardVolumeChanged(int new_value)260{261// only called for base settings262DebugAssert(!m_dialog->isPerGameSettings());263Host::SetBaseIntSettingValue("Audio", "FastForwardVolume", new_value);264Host::CommitBaseSettingChanges();265g_emu_thread->setAudioOutputVolume(m_ui.volume->value(), new_value);266267updateVolumeLabel();268}269270void AudioSettingsWidget::onOutputMutedChanged(int new_state)271{272// only called for base settings273DebugAssert(!m_dialog->isPerGameSettings());274275const bool muted = (new_state != 0);276Host::SetBaseBoolSettingValue("Audio", "OutputMuted", muted);277Host::CommitBaseSettingChanges();278g_emu_thread->setAudioOutputMuted(muted);279}280281void AudioSettingsWidget::onStretchSettingsClicked()282{283QDialog dlg(QtUtils::GetRootWidget(this));284Ui::AudioStretchSettingsDialog dlgui;285dlgui.setupUi(&dlg);286dlgui.icon->setPixmap(QIcon::fromTheme(QStringLiteral("volume-up-line")).pixmap(32));287dlgui.buttonBox->button(QDialogButtonBox::Close)->setDefault(true);288289SettingsInterface* sif = m_dialog->getSettingsInterface();290SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.sequenceLength, "Audio", "StretchSequenceLengthMS",291AudioStreamParameters::DEFAULT_STRETCH_SEQUENCE_LENGTH, 0);292QtUtils::BindLabelToSlider(dlgui.sequenceLength, dlgui.sequenceLengthLabel);293SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.seekWindowSize, "Audio", "StretchSeekWindowMS",294AudioStreamParameters::DEFAULT_STRETCH_SEEKWINDOW, 0);295QtUtils::BindLabelToSlider(dlgui.seekWindowSize, dlgui.seekWindowSizeLabel);296SettingWidgetBinder::BindWidgetToIntSetting(sif, dlgui.overlap, "Audio", "StretchOverlapMS",297AudioStreamParameters::DEFAULT_STRETCH_OVERLAP, 0);298QtUtils::BindLabelToSlider(dlgui.overlap, dlgui.overlapLabel);299SettingWidgetBinder::BindWidgetToBoolSetting(sif, dlgui.useQuickSeek, "Audio", "StretchUseQuickSeek",300AudioStreamParameters::DEFAULT_STRETCH_USE_QUICKSEEK);301SettingWidgetBinder::BindWidgetToBoolSetting(sif, dlgui.useAAFilter, "Audio", "StretchUseAAFilter",302AudioStreamParameters::DEFAULT_STRETCH_USE_AA_FILTER);303304connect(dlgui.buttonBox, &QDialogButtonBox::rejected, &dlg, &QDialog::accept);305connect(dlgui.buttonBox->button(QDialogButtonBox::RestoreDefaults), &QPushButton::clicked, this, [this, &dlg]() {306m_dialog->setIntSettingValue("Audio", "StretchSequenceLengthMS",307m_dialog->isPerGameSettings() ?308std::nullopt :309std::optional<int>(AudioStreamParameters::DEFAULT_STRETCH_SEQUENCE_LENGTH));310m_dialog->setIntSettingValue("Audio", "StretchSeekWindowMS",311m_dialog->isPerGameSettings() ?312std::nullopt :313std::optional<int>(AudioStreamParameters::DEFAULT_STRETCH_SEEKWINDOW));314m_dialog->setIntSettingValue("Audio", "StretchOverlapMS",315m_dialog->isPerGameSettings() ?316std::nullopt :317std::optional<int>(AudioStreamParameters::DEFAULT_STRETCH_OVERLAP));318m_dialog->setBoolSettingValue("Audio", "StretchUseQuickSeek",319m_dialog->isPerGameSettings() ?320std::nullopt :321std::optional<bool>(AudioStreamParameters::DEFAULT_STRETCH_USE_QUICKSEEK));322m_dialog->setBoolSettingValue("Audio", "StretchUseAAFilter",323m_dialog->isPerGameSettings() ?324std::nullopt :325std::optional<bool>(AudioStreamParameters::DEFAULT_STRETCH_USE_AA_FILTER));326327dlg.reject();328329QMetaObject::invokeMethod(this, &AudioSettingsWidget::onStretchSettingsClicked, Qt::QueuedConnection);330});331332dlg.exec();333}334335void AudioSettingsWidget::resetVolume(bool fast_forward)336{337const char* key = fast_forward ? "FastForwardVolume" : "OutputVolume";338QSlider* const slider = fast_forward ? m_ui.fastForwardVolume : m_ui.volume;339QLabel* const label = fast_forward ? m_ui.fastForwardVolumeLabel : m_ui.volumeLabel;340341if (m_dialog->isPerGameSettings())342{343m_dialog->removeSettingValue("Audio", key);344345const int value = m_dialog->getEffectiveIntValue("Audio", key, 100);346QSignalBlocker sb(slider);347slider->setValue(value);348label->setText(QStringLiteral("%1%2").arg(value).arg(tr("%")));349350// remove bold font if it was previously overridden351QFont font(label->font());352font.setBold(false);353label->setFont(font);354}355else356{357slider->setValue(100);358}359}360361362