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