]> git.tdb.fi Git - ext/openal.git/blob - utils/alsoft-config/mainwindow.cpp
Import OpenAL Soft 1.23.1 sources
[ext/openal.git] / utils / alsoft-config / mainwindow.cpp
1
2 #include "config.h"
3
4 #include "mainwindow.h"
5
6 #include <iostream>
7 #include <cmath>
8
9 #include <QFileDialog>
10 #include <QMessageBox>
11 #include <QCloseEvent>
12 #include <QSettings>
13 #include <QtGlobal>
14 #include "ui_mainwindow.h"
15 #include "verstr.h"
16
17 #ifdef _WIN32
18 #include <windows.h>
19 #include <shlobj.h>
20 #endif
21
22 namespace {
23
24 const struct {
25     char backend_name[16];
26     char full_string[32];
27 } backendList[] = {
28 #ifdef HAVE_JACK
29     { "jack", "JACK" },
30 #endif
31 #ifdef HAVE_PIPEWIRE
32     { "pipewire", "PipeWire" },
33 #endif
34 #ifdef HAVE_PULSEAUDIO
35     { "pulse", "PulseAudio" },
36 #endif
37 #ifdef HAVE_ALSA
38     { "alsa", "ALSA" },
39 #endif
40 #ifdef HAVE_COREAUDIO
41     { "core", "CoreAudio" },
42 #endif
43 #ifdef HAVE_OSS
44     { "oss", "OSS" },
45 #endif
46 #ifdef HAVE_SOLARIS
47     { "solaris", "Solaris" },
48 #endif
49 #ifdef HAVE_SNDIO
50     { "sndio", "SoundIO" },
51 #endif
52 #ifdef HAVE_QSA
53     { "qsa", "QSA" },
54 #endif
55 #ifdef HAVE_WASAPI
56     { "wasapi", "WASAPI" },
57 #endif
58 #ifdef HAVE_DSOUND
59     { "dsound", "DirectSound" },
60 #endif
61 #ifdef HAVE_WINMM
62     { "winmm", "Windows Multimedia" },
63 #endif
64 #ifdef HAVE_PORTAUDIO
65     { "port", "PortAudio" },
66 #endif
67 #ifdef HAVE_OPENSL
68     { "opensl", "OpenSL" },
69 #endif
70
71     { "null", "Null Output" },
72 #ifdef HAVE_WAVE
73     { "wave", "Wave Writer" },
74 #endif
75     { "", "" }
76 };
77
78 const struct NameValuePair {
79     const char name[64];
80     const char value[16];
81 } speakerModeList[] = {
82     { "Autodetect", "" },
83     { "Mono", "mono" },
84     { "Stereo", "stereo" },
85     { "Quadraphonic", "quad" },
86     { "5.1 Surround", "surround51" },
87     { "6.1 Surround", "surround61" },
88     { "7.1 Surround", "surround71" },
89     { "3D7.1 Surround", "surround3d71" },
90
91     { "Ambisonic, 1st Order", "ambi1" },
92     { "Ambisonic, 2nd Order", "ambi2" },
93     { "Ambisonic, 3rd Order", "ambi3" },
94
95     { "", "" }
96 }, sampleTypeList[] = {
97     { "Autodetect", "" },
98     { "8-bit int", "int8" },
99     { "8-bit uint", "uint8" },
100     { "16-bit int", "int16" },
101     { "16-bit uint", "uint16" },
102     { "32-bit int", "int32" },
103     { "32-bit uint", "uint32" },
104     { "32-bit float", "float32" },
105
106     { "", "" }
107 }, resamplerList[] = {
108     { "Point", "point" },
109     { "Linear", "linear" },
110     { "Cubic Spline", "cubic" },
111     { "Default (Cubic Spline)", "" },
112     { "11th order Sinc (fast)", "fast_bsinc12" },
113     { "11th order Sinc", "bsinc12" },
114     { "23rd order Sinc (fast)", "fast_bsinc24" },
115     { "23rd order Sinc", "bsinc24" },
116
117     { "", "" }
118 }, stereoModeList[] = {
119     { "Autodetect", "" },
120     { "Speakers", "speakers" },
121     { "Headphones", "headphones" },
122
123     { "", "" }
124 }, stereoEncList[] = {
125     { "Default", "" },
126     { "Basic", "panpot" },
127     { "UHJ", "uhj" },
128     { "Binaural", "hrtf" },
129
130     { "", "" }
131 }, ambiFormatList[] = {
132     { "Default", "" },
133     { "AmbiX (ACN, SN3D)", "ambix" },
134     { "Furse-Malham", "fuma" },
135     { "ACN, N3D", "acn+n3d" },
136     { "ACN, FuMa", "acn+fuma" },
137
138     { "", "" }
139 }, hrtfModeList[] = {
140     { "1st Order Ambisonic", "ambi1" },
141     { "2nd Order Ambisonic", "ambi2" },
142     { "3rd Order Ambisonic", "ambi3" },
143     { "Default (Full)", "" },
144     { "Full", "full" },
145
146     { "", "" }
147 };
148
149 QString getDefaultConfigName()
150 {
151 #ifdef Q_OS_WIN32
152     static const char fname[] = "alsoft.ini";
153     auto get_appdata_path = []() noexcept -> QString
154     {
155         WCHAR buffer[MAX_PATH];
156         if(SHGetSpecialFolderPathW(nullptr, buffer, CSIDL_APPDATA, FALSE) != FALSE)
157             return QString::fromWCharArray(buffer);
158         return QString();
159     };
160     QString base = get_appdata_path();
161 #else
162     static const char fname[] = "alsoft.conf";
163     QByteArray base = qgetenv("XDG_CONFIG_HOME");
164     if(base.isEmpty())
165     {
166         base = qgetenv("HOME");
167         if(base.isEmpty() == false)
168             base += "/.config";
169     }
170 #endif
171     if(base.isEmpty() == false)
172         return base +'/'+ fname;
173     return fname;
174 }
175
176 QString getBaseDataPath()
177 {
178 #ifdef Q_OS_WIN32
179     auto get_appdata_path = []() noexcept -> QString
180     {
181         WCHAR buffer[MAX_PATH];
182         if(SHGetSpecialFolderPathW(nullptr, buffer, CSIDL_APPDATA, FALSE) != FALSE)
183             return QString::fromWCharArray(buffer);
184         return QString();
185     };
186     QString base = get_appdata_path();
187 #else
188     QByteArray base = qgetenv("XDG_DATA_HOME");
189     if(base.isEmpty())
190     {
191         base = qgetenv("HOME");
192         if(!base.isEmpty())
193             base += "/.local/share";
194     }
195 #endif
196     return base;
197 }
198
199 QStringList getAllDataPaths(const QString &append)
200 {
201     QStringList list;
202     list.append(getBaseDataPath());
203 #ifdef Q_OS_WIN32
204     // TODO: Common AppData path
205 #else
206     QString paths = qgetenv("XDG_DATA_DIRS");
207     if(paths.isEmpty())
208         paths = "/usr/local/share/:/usr/share/";
209 #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
210     list += paths.split(QChar(':'), Qt::SkipEmptyParts);
211 #else
212     list += paths.split(QChar(':'), QString::SkipEmptyParts);
213 #endif
214 #endif
215     QStringList::iterator iter = list.begin();
216     while(iter != list.end())
217     {
218         if(iter->isEmpty())
219             iter = list.erase(iter);
220         else
221         {
222             iter->append(append);
223             iter++;
224         }
225     }
226     return list;
227 }
228
229 template<size_t N>
230 QString getValueFromName(const NameValuePair (&list)[N], const QString &str)
231 {
232     for(size_t i = 0;i < N-1;i++)
233     {
234         if(str == list[i].name)
235             return list[i].value;
236     }
237     return QString{};
238 }
239
240 template<size_t N>
241 QString getNameFromValue(const NameValuePair (&list)[N], const QString &str)
242 {
243     for(size_t i = 0;i < N-1;i++)
244     {
245         if(str == list[i].value)
246             return list[i].name;
247     }
248     return QString{};
249 }
250
251
252 Qt::CheckState getCheckState(const QVariant &var)
253 {
254     if(var.isNull())
255         return Qt::PartiallyChecked;
256     if(var.toBool())
257         return Qt::Checked;
258     return Qt::Unchecked;
259 }
260
261 QString getCheckValue(const QCheckBox *checkbox)
262 {
263     const Qt::CheckState state{checkbox->checkState()};
264     if(state == Qt::Checked)
265         return QString{"true"};
266     if(state == Qt::Unchecked)
267         return QString{"false"};
268     return QString{};
269 }
270
271 }
272
273 MainWindow::MainWindow(QWidget *parent) :
274     QMainWindow(parent),
275     ui(new Ui::MainWindow),
276     mPeriodSizeValidator(nullptr),
277     mPeriodCountValidator(nullptr),
278     mSourceCountValidator(nullptr),
279     mEffectSlotValidator(nullptr),
280     mSourceSendValidator(nullptr),
281     mSampleRateValidator(nullptr),
282     mJackBufferValidator(nullptr),
283     mNeedsSave(false)
284 {
285     ui->setupUi(this);
286
287     for(int i = 0;speakerModeList[i].name[0];i++)
288         ui->channelConfigCombo->addItem(speakerModeList[i].name);
289     ui->channelConfigCombo->adjustSize();
290     for(int i = 0;sampleTypeList[i].name[0];i++)
291         ui->sampleFormatCombo->addItem(sampleTypeList[i].name);
292     ui->sampleFormatCombo->adjustSize();
293     for(int i = 0;stereoModeList[i].name[0];i++)
294         ui->stereoModeCombo->addItem(stereoModeList[i].name);
295     ui->stereoModeCombo->adjustSize();
296     for(int i = 0;stereoEncList[i].name[0];i++)
297         ui->stereoEncodingComboBox->addItem(stereoEncList[i].name);
298     ui->stereoEncodingComboBox->adjustSize();
299     for(int i = 0;ambiFormatList[i].name[0];i++)
300         ui->ambiFormatComboBox->addItem(ambiFormatList[i].name);
301     ui->ambiFormatComboBox->adjustSize();
302
303     int count;
304     for(count = 0;resamplerList[count].name[0];count++) {
305     }
306     ui->resamplerSlider->setRange(0, count-1);
307
308     for(count = 0;hrtfModeList[count].name[0];count++) {
309     }
310     ui->hrtfmodeSlider->setRange(0, count-1);
311
312 #if !defined(HAVE_NEON) && !defined(HAVE_SSE)
313     ui->cpuExtDisabledLabel->move(ui->cpuExtDisabledLabel->x(), ui->cpuExtDisabledLabel->y() - 60);
314 #else
315     ui->cpuExtDisabledLabel->setVisible(false);
316 #endif
317
318 #ifndef HAVE_NEON
319
320 #ifndef HAVE_SSE4_1
321 #ifndef HAVE_SSE3
322 #ifndef HAVE_SSE2
323 #ifndef HAVE_SSE
324     ui->enableSSECheckBox->setVisible(false);
325 #endif /* !SSE */
326     ui->enableSSE2CheckBox->setVisible(false);
327 #endif /* !SSE2 */
328     ui->enableSSE3CheckBox->setVisible(false);
329 #endif /* !SSE3 */
330     ui->enableSSE41CheckBox->setVisible(false);
331 #endif /* !SSE4.1 */
332     ui->enableNeonCheckBox->setVisible(false);
333
334 #else /* !Neon */
335
336 #ifndef HAVE_SSE4_1
337 #ifndef HAVE_SSE3
338 #ifndef HAVE_SSE2
339 #ifndef HAVE_SSE
340     ui->enableNeonCheckBox->move(ui->enableNeonCheckBox->x(), ui->enableNeonCheckBox->y() - 30);
341     ui->enableSSECheckBox->setVisible(false);
342 #endif /* !SSE */
343     ui->enableSSE2CheckBox->setVisible(false);
344 #endif /* !SSE2 */
345     ui->enableSSE3CheckBox->setVisible(false);
346 #endif /* !SSE3 */
347     ui->enableSSE41CheckBox->setVisible(false);
348 #endif /* !SSE4.1 */
349
350 #endif
351
352 #ifndef ALSOFT_EAX
353     ui->enableEaxCheck->setChecked(Qt::Unchecked);
354     ui->enableEaxCheck->setEnabled(false);
355     ui->enableEaxCheck->setVisible(false);
356 #endif
357
358     mPeriodSizeValidator = new QIntValidator{64, 8192, this};
359     ui->periodSizeEdit->setValidator(mPeriodSizeValidator);
360     mPeriodCountValidator = new QIntValidator{2, 16, this};
361     ui->periodCountEdit->setValidator(mPeriodCountValidator);
362
363     mSourceCountValidator = new QIntValidator{0, 4096, this};
364     ui->srcCountLineEdit->setValidator(mSourceCountValidator);
365     mEffectSlotValidator = new QIntValidator{0, 64, this};
366     ui->effectSlotLineEdit->setValidator(mEffectSlotValidator);
367     mSourceSendValidator = new QIntValidator{0, 16, this};
368     ui->srcSendLineEdit->setValidator(mSourceSendValidator);
369     mSampleRateValidator = new QIntValidator{8000, 192000, this};
370     ui->sampleRateCombo->lineEdit()->setValidator(mSampleRateValidator);
371
372     mJackBufferValidator = new QIntValidator{0, 8192, this};
373     ui->jackBufferSizeLine->setValidator(mJackBufferValidator);
374
375     connect(ui->actionLoad, &QAction::triggered, this, &MainWindow::loadConfigFromFile);
376     connect(ui->actionSave_As, &QAction::triggered, this, &MainWindow::saveConfigAsFile);
377
378     connect(ui->actionAbout, &QAction::triggered, this, &MainWindow::showAboutPage);
379
380     connect(ui->closeCancelButton, &QPushButton::clicked, this, &MainWindow::cancelCloseAction);
381     connect(ui->applyButton, &QPushButton::clicked, this, &MainWindow::saveCurrentConfig);
382
383     auto qcb_cicint = static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged);
384     connect(ui->channelConfigCombo, qcb_cicint, this, &MainWindow::enableApplyButton);
385     connect(ui->sampleFormatCombo, qcb_cicint, this, &MainWindow::enableApplyButton);
386     connect(ui->stereoModeCombo, qcb_cicint, this, &MainWindow::enableApplyButton);
387     connect(ui->sampleRateCombo, qcb_cicint, this, &MainWindow::enableApplyButton);
388     connect(ui->sampleRateCombo, &QComboBox::editTextChanged, this, &MainWindow::enableApplyButton);
389
390     connect(ui->resamplerSlider, &QSlider::valueChanged, this, &MainWindow::updateResamplerLabel);
391
392     connect(ui->periodSizeSlider, &QSlider::valueChanged, this, &MainWindow::updatePeriodSizeEdit);
393     connect(ui->periodSizeEdit, &QLineEdit::editingFinished, this, &MainWindow::updatePeriodSizeSlider);
394     connect(ui->periodCountSlider, &QSlider::valueChanged, this, &MainWindow::updatePeriodCountEdit);
395     connect(ui->periodCountEdit, &QLineEdit::editingFinished, this, &MainWindow::updatePeriodCountSlider);
396
397     connect(ui->stereoEncodingComboBox, qcb_cicint, this, &MainWindow::enableApplyButton);
398     connect(ui->ambiFormatComboBox, qcb_cicint, this, &MainWindow::enableApplyButton);
399     connect(ui->outputLimiterCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
400     connect(ui->outputDitherCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
401
402     connect(ui->decoderHQModeCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
403     connect(ui->decoderDistCompCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
404     connect(ui->decoderNFEffectsCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
405     auto qdsb_vcd = static_cast<void(QDoubleSpinBox::*)(double)>(&QDoubleSpinBox::valueChanged);
406     connect(ui->decoderSpeakerDistSpinBox, qdsb_vcd, this, &MainWindow::enableApplyButton);
407     connect(ui->decoderQuadLineEdit, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton);
408     connect(ui->decoderQuadButton, &QPushButton::clicked, this, &MainWindow::selectQuadDecoderFile);
409     connect(ui->decoder51LineEdit, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton);
410     connect(ui->decoder51Button, &QPushButton::clicked, this, &MainWindow::select51DecoderFile);
411     connect(ui->decoder61LineEdit, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton);
412     connect(ui->decoder61Button, &QPushButton::clicked, this, &MainWindow::select61DecoderFile);
413     connect(ui->decoder71LineEdit, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton);
414     connect(ui->decoder71Button, &QPushButton::clicked, this, &MainWindow::select71DecoderFile);
415     connect(ui->decoder3D71LineEdit, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton);
416     connect(ui->decoder3D71Button, &QPushButton::clicked, this, &MainWindow::select3D71DecoderFile);
417
418     connect(ui->preferredHrtfComboBox, qcb_cicint, this, &MainWindow::enableApplyButton);
419     connect(ui->hrtfmodeSlider, &QSlider::valueChanged, this, &MainWindow::updateHrtfModeLabel);
420
421     connect(ui->hrtfAddButton, &QPushButton::clicked, this, &MainWindow::addHrtfFile);
422     connect(ui->hrtfRemoveButton, &QPushButton::clicked, this, &MainWindow::removeHrtfFile);
423     connect(ui->hrtfFileList, &QListWidget::itemSelectionChanged, this, &MainWindow::updateHrtfRemoveButton);
424     connect(ui->defaultHrtfPathsCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
425
426     connect(ui->srcCountLineEdit, &QLineEdit::editingFinished, this, &MainWindow::enableApplyButton);
427     connect(ui->srcSendLineEdit, &QLineEdit::editingFinished, this, &MainWindow::enableApplyButton);
428     connect(ui->effectSlotLineEdit, &QLineEdit::editingFinished, this, &MainWindow::enableApplyButton);
429
430     connect(ui->enableSSECheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
431     connect(ui->enableSSE2CheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
432     connect(ui->enableSSE3CheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
433     connect(ui->enableSSE41CheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
434     connect(ui->enableNeonCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
435
436     ui->enabledBackendList->setContextMenuPolicy(Qt::CustomContextMenu);
437     connect(ui->enabledBackendList, &QListWidget::customContextMenuRequested, this, &MainWindow::showEnabledBackendMenu);
438
439     ui->disabledBackendList->setContextMenuPolicy(Qt::CustomContextMenu);
440     connect(ui->disabledBackendList, &QListWidget::customContextMenuRequested, this, &MainWindow::showDisabledBackendMenu);
441     connect(ui->backendCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
442
443     connect(ui->defaultReverbComboBox, qcb_cicint, this, &MainWindow::enableApplyButton);
444     connect(ui->enableEaxReverbCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
445     connect(ui->enableStdReverbCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
446     connect(ui->enableAutowahCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
447     connect(ui->enableChorusCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
448     connect(ui->enableCompressorCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
449     connect(ui->enableDistortionCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
450     connect(ui->enableEchoCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
451     connect(ui->enableEqualizerCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
452     connect(ui->enableFlangerCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
453     connect(ui->enableFrequencyShifterCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
454     connect(ui->enableModulatorCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
455     connect(ui->enableDedicatedCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
456     connect(ui->enablePitchShifterCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
457     connect(ui->enableVocalMorpherCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
458     connect(ui->enableEaxCheck, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
459
460     connect(ui->pulseAutospawnCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
461     connect(ui->pulseAllowMovesCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
462     connect(ui->pulseFixRateCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
463     connect(ui->pulseAdjLatencyCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
464
465     connect(ui->pwireAssumeAudioCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
466     connect(ui->pwireRtMixCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
467
468     connect(ui->wasapiResamplerCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
469
470     connect(ui->jackAutospawnCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
471     connect(ui->jackConnectPortsCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
472     connect(ui->jackRtMixCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
473     connect(ui->jackBufferSizeSlider, &QSlider::valueChanged, this, &MainWindow::updateJackBufferSizeEdit);
474     connect(ui->jackBufferSizeLine, &QLineEdit::editingFinished, this, &MainWindow::updateJackBufferSizeSlider);
475
476     connect(ui->alsaDefaultDeviceLine, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton);
477     connect(ui->alsaDefaultCaptureLine, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton);
478     connect(ui->alsaResamplerCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
479     connect(ui->alsaMmapCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
480
481     connect(ui->ossDefaultDeviceLine, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton);
482     connect(ui->ossPlaybackPushButton, &QPushButton::clicked, this, &MainWindow::selectOSSPlayback);
483     connect(ui->ossDefaultCaptureLine, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton);
484     connect(ui->ossCapturePushButton, &QPushButton::clicked, this, &MainWindow::selectOSSCapture);
485
486     connect(ui->solarisDefaultDeviceLine, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton);
487     connect(ui->solarisPlaybackPushButton, &QPushButton::clicked, this, &MainWindow::selectSolarisPlayback);
488
489     connect(ui->waveOutputLine, &QLineEdit::textChanged, this, &MainWindow::enableApplyButton);
490     connect(ui->waveOutputButton, &QPushButton::clicked, this, &MainWindow::selectWaveOutput);
491     connect(ui->waveBFormatCheckBox, &QCheckBox::stateChanged, this, &MainWindow::enableApplyButton);
492
493     ui->backendListWidget->setCurrentRow(0);
494     ui->tabWidget->setCurrentIndex(0);
495
496     for(int i = 1;i < ui->backendListWidget->count();i++)
497         ui->backendListWidget->setRowHidden(i, true);
498     for(int i = 0;backendList[i].backend_name[0];i++)
499     {
500         QList<QListWidgetItem*> items = ui->backendListWidget->findItems(
501             backendList[i].full_string, Qt::MatchFixedString);
502         foreach(QListWidgetItem *item, items)
503             item->setHidden(false);
504     }
505
506     loadConfig(getDefaultConfigName());
507 }
508
509 MainWindow::~MainWindow()
510 {
511     delete ui;
512     delete mPeriodSizeValidator;
513     delete mPeriodCountValidator;
514     delete mSourceCountValidator;
515     delete mEffectSlotValidator;
516     delete mSourceSendValidator;
517     delete mSampleRateValidator;
518     delete mJackBufferValidator;
519 }
520
521 void MainWindow::closeEvent(QCloseEvent *event)
522 {
523     if(!mNeedsSave)
524         event->accept();
525     else
526     {
527         QMessageBox::StandardButton btn = QMessageBox::warning(this,
528             tr("Apply changes?"), tr("Save changes before quitting?"),
529             QMessageBox::Save | QMessageBox::No | QMessageBox::Cancel);
530         if(btn == QMessageBox::Save)
531             saveCurrentConfig();
532         if(btn == QMessageBox::Cancel)
533             event->ignore();
534         else
535             event->accept();
536     }
537 }
538
539 void MainWindow::cancelCloseAction()
540 {
541     mNeedsSave = false;
542     close();
543 }
544
545
546 void MainWindow::showAboutPage()
547 {
548     QMessageBox::information(this, tr("About"),
549         tr("OpenAL Soft Configuration Utility.\nBuilt for OpenAL Soft library version ") +
550         GetVersionString());
551 }
552
553
554 QStringList MainWindow::collectHrtfs()
555 {
556     QStringList ret;
557     QStringList processed;
558
559     for(int i = 0;i < ui->hrtfFileList->count();i++)
560     {
561         QDir dir(ui->hrtfFileList->item(i)->text());
562         QStringList fnames = dir.entryList(QDir::Files | QDir::Readable, QDir::Name);
563         foreach(const QString &fname, fnames)
564         {
565             if(!fname.endsWith(".mhr", Qt::CaseInsensitive))
566                 continue;
567             QString fullname{dir.absoluteFilePath(fname)};
568             if(processed.contains(fullname))
569                 continue;
570             processed.push_back(fullname);
571
572             QString name{fname.left(fname.length()-4)};
573             if(!ret.contains(name))
574                 ret.push_back(name);
575             else
576             {
577                 size_t i{2};
578                 do {
579                     QString s = name+" #"+QString::number(i);
580                     if(!ret.contains(s))
581                     {
582                         ret.push_back(s);
583                         break;
584                     }
585                     ++i;
586                 } while(1);
587             }
588         }
589     }
590
591     if(ui->defaultHrtfPathsCheckBox->isChecked())
592     {
593         QStringList paths = getAllDataPaths("/openal/hrtf");
594         foreach(const QString &name, paths)
595         {
596             QDir dir{name};
597             QStringList fnames{dir.entryList(QDir::Files | QDir::Readable, QDir::Name)};
598             foreach(const QString &fname, fnames)
599             {
600                 if(!fname.endsWith(".mhr", Qt::CaseInsensitive))
601                     continue;
602                 QString fullname{dir.absoluteFilePath(fname)};
603                 if(processed.contains(fullname))
604                     continue;
605                 processed.push_back(fullname);
606
607                 QString name{fname.left(fname.length()-4)};
608                 if(!ret.contains(name))
609                     ret.push_back(name);
610                 else
611                 {
612                     size_t i{2};
613                     do {
614                         QString s{name+" #"+QString::number(i)};
615                         if(!ret.contains(s))
616                         {
617                             ret.push_back(s);
618                             break;
619                         }
620                         ++i;
621                     } while(1);
622                 }
623             }
624         }
625
626 #ifdef ALSOFT_EMBED_HRTF_DATA
627         ret.push_back("Built-In HRTF");
628 #endif
629     }
630     return ret;
631 }
632
633
634 void MainWindow::loadConfigFromFile()
635 {
636     QString fname = QFileDialog::getOpenFileName(this, tr("Select Files"));
637     if(fname.isEmpty() == false)
638         loadConfig(fname);
639 }
640
641 void MainWindow::loadConfig(const QString &fname)
642 {
643     QSettings settings{fname, QSettings::IniFormat};
644
645     QString sampletype = settings.value("sample-type").toString();
646     ui->sampleFormatCombo->setCurrentIndex(0);
647     if(sampletype.isEmpty() == false)
648     {
649         QString str{getNameFromValue(sampleTypeList, sampletype)};
650         if(!str.isEmpty())
651         {
652             const int j{ui->sampleFormatCombo->findText(str)};
653             if(j > 0) ui->sampleFormatCombo->setCurrentIndex(j);
654         }
655     }
656
657     QString channelconfig{settings.value("channels").toString()};
658     ui->channelConfigCombo->setCurrentIndex(0);
659     if(channelconfig.isEmpty() == false)
660     {
661         if(channelconfig == "surround51rear")
662             channelconfig = "surround51";
663         QString str{getNameFromValue(speakerModeList, channelconfig)};
664         if(!str.isEmpty())
665         {
666             const int j{ui->channelConfigCombo->findText(str)};
667             if(j > 0) ui->channelConfigCombo->setCurrentIndex(j);
668         }
669     }
670
671     QString srate{settings.value("frequency").toString()};
672     if(srate.isEmpty())
673         ui->sampleRateCombo->setCurrentIndex(0);
674     else
675     {
676         ui->sampleRateCombo->lineEdit()->clear();
677         ui->sampleRateCombo->lineEdit()->insert(srate);
678     }
679
680     ui->srcCountLineEdit->clear();
681     ui->srcCountLineEdit->insert(settings.value("sources").toString());
682     ui->effectSlotLineEdit->clear();
683     ui->effectSlotLineEdit->insert(settings.value("slots").toString());
684     ui->srcSendLineEdit->clear();
685     ui->srcSendLineEdit->insert(settings.value("sends").toString());
686
687     QString resampler = settings.value("resampler").toString().trimmed();
688     ui->resamplerSlider->setValue(2);
689     ui->resamplerLabel->setText(resamplerList[2].name);
690     /* The "sinc4" and "sinc8" resamplers are no longer supported. Use "cubic"
691      * as a fallback.
692      */
693     if(resampler == "sinc4" || resampler == "sinc8")
694         resampler = "cubic";
695     /* The "bsinc" resampler name is an alias for "bsinc12". */
696     else if(resampler == "bsinc")
697         resampler = "bsinc12";
698     for(int i = 0;resamplerList[i].name[0];i++)
699     {
700         if(resampler == resamplerList[i].value)
701         {
702             ui->resamplerSlider->setValue(i);
703             ui->resamplerLabel->setText(resamplerList[i].name);
704             break;
705         }
706     }
707
708     QString stereomode = settings.value("stereo-mode").toString().trimmed();
709     ui->stereoModeCombo->setCurrentIndex(0);
710     if(stereomode.isEmpty() == false)
711     {
712         QString str{getNameFromValue(stereoModeList, stereomode)};
713         if(!str.isEmpty())
714         {
715             const int j{ui->stereoModeCombo->findText(str)};
716             if(j > 0) ui->stereoModeCombo->setCurrentIndex(j);
717         }
718     }
719
720     int periodsize{settings.value("period_size").toInt()};
721     ui->periodSizeEdit->clear();
722     if(periodsize >= 64)
723     {
724         ui->periodSizeEdit->insert(QString::number(periodsize));
725         updatePeriodSizeSlider();
726     }
727
728     int periodcount{settings.value("periods").toInt()};
729     ui->periodCountEdit->clear();
730     if(periodcount >= 2)
731     {
732         ui->periodCountEdit->insert(QString::number(periodcount));
733         updatePeriodCountSlider();
734     }
735
736     ui->outputLimiterCheckBox->setCheckState(getCheckState(settings.value("output-limiter")));
737     ui->outputDitherCheckBox->setCheckState(getCheckState(settings.value("dither")));
738
739     QString stereopan{settings.value("stereo-encoding").toString()};
740     ui->stereoEncodingComboBox->setCurrentIndex(0);
741     if(stereopan.isEmpty() == false)
742     {
743         QString str{getNameFromValue(stereoEncList, stereopan)};
744         if(!str.isEmpty())
745         {
746             const int j{ui->stereoEncodingComboBox->findText(str)};
747             if(j > 0) ui->stereoEncodingComboBox->setCurrentIndex(j);
748         }
749     }
750
751     QString ambiformat{settings.value("ambi-format").toString()};
752     ui->ambiFormatComboBox->setCurrentIndex(0);
753     if(ambiformat.isEmpty() == false)
754     {
755         QString str{getNameFromValue(ambiFormatList, ambiformat)};
756         if(!str.isEmpty())
757         {
758             const int j{ui->ambiFormatComboBox->findText(str)};
759             if(j > 0) ui->ambiFormatComboBox->setCurrentIndex(j);
760         }
761     }
762
763     ui->decoderHQModeCheckBox->setChecked(getCheckState(settings.value("decoder/hq-mode")));
764     ui->decoderDistCompCheckBox->setCheckState(getCheckState(settings.value("decoder/distance-comp")));
765     ui->decoderNFEffectsCheckBox->setCheckState(getCheckState(settings.value("decoder/nfc")));
766     double speakerdist{settings.value("decoder/speaker-dist", 1.0).toDouble()};
767     ui->decoderSpeakerDistSpinBox->setValue(speakerdist);
768
769     ui->decoderQuadLineEdit->setText(settings.value("decoder/quad").toString());
770     ui->decoder51LineEdit->setText(settings.value("decoder/surround51").toString());
771     ui->decoder61LineEdit->setText(settings.value("decoder/surround61").toString());
772     ui->decoder71LineEdit->setText(settings.value("decoder/surround71").toString());
773     ui->decoder3D71LineEdit->setText(settings.value("decoder/surround3d71").toString());
774
775     QStringList disabledCpuExts{settings.value("disable-cpu-exts").toStringList()};
776     if(disabledCpuExts.size() == 1)
777         disabledCpuExts = disabledCpuExts[0].split(QChar(','));
778     for(QString &name : disabledCpuExts)
779         name = name.trimmed();
780     ui->enableSSECheckBox->setChecked(!disabledCpuExts.contains("sse", Qt::CaseInsensitive));
781     ui->enableSSE2CheckBox->setChecked(!disabledCpuExts.contains("sse2", Qt::CaseInsensitive));
782     ui->enableSSE3CheckBox->setChecked(!disabledCpuExts.contains("sse3", Qt::CaseInsensitive));
783     ui->enableSSE41CheckBox->setChecked(!disabledCpuExts.contains("sse4.1", Qt::CaseInsensitive));
784     ui->enableNeonCheckBox->setChecked(!disabledCpuExts.contains("neon", Qt::CaseInsensitive));
785
786     QString hrtfmode{settings.value("hrtf-mode").toString().trimmed()};
787     ui->hrtfmodeSlider->setValue(2);
788     ui->hrtfmodeLabel->setText(hrtfModeList[3].name);
789     /* The "basic" mode name is no longer supported. Use "ambi2" instead. */
790     if(hrtfmode == "basic")
791         hrtfmode = "ambi2";
792     for(int i = 0;hrtfModeList[i].name[0];i++)
793     {
794         if(hrtfmode == hrtfModeList[i].value)
795         {
796             ui->hrtfmodeSlider->setValue(i);
797             ui->hrtfmodeLabel->setText(hrtfModeList[i].name);
798             break;
799         }
800     }
801
802     QStringList hrtf_paths{settings.value("hrtf-paths").toStringList()};
803     if(hrtf_paths.size() == 1)
804         hrtf_paths = hrtf_paths[0].split(QChar(','));
805     for(QString &name : hrtf_paths)
806         name = name.trimmed();
807     if(!hrtf_paths.empty() && !hrtf_paths.back().isEmpty())
808         ui->defaultHrtfPathsCheckBox->setCheckState(Qt::Unchecked);
809     else
810     {
811         hrtf_paths.removeAll(QString());
812         ui->defaultHrtfPathsCheckBox->setCheckState(Qt::Checked);
813     }
814     hrtf_paths.removeDuplicates();
815     ui->hrtfFileList->clear();
816     ui->hrtfFileList->addItems(hrtf_paths);
817     updateHrtfRemoveButton();
818
819     ui->preferredHrtfComboBox->clear();
820     ui->preferredHrtfComboBox->addItem("- Any -");
821     if(ui->defaultHrtfPathsCheckBox->isChecked())
822     {
823         QStringList hrtfs{collectHrtfs()};
824         foreach(const QString &name, hrtfs)
825             ui->preferredHrtfComboBox->addItem(name);
826     }
827
828     QString defaulthrtf{settings.value("default-hrtf").toString()};
829     ui->preferredHrtfComboBox->setCurrentIndex(0);
830     if(defaulthrtf.isEmpty() == false)
831     {
832         int i{ui->preferredHrtfComboBox->findText(defaulthrtf)};
833         if(i > 0)
834             ui->preferredHrtfComboBox->setCurrentIndex(i);
835         else
836         {
837             i = ui->preferredHrtfComboBox->count();
838             ui->preferredHrtfComboBox->addItem(defaulthrtf);
839             ui->preferredHrtfComboBox->setCurrentIndex(i);
840         }
841     }
842     ui->preferredHrtfComboBox->adjustSize();
843
844     ui->enabledBackendList->clear();
845     ui->disabledBackendList->clear();
846     QStringList drivers{settings.value("drivers").toStringList()};
847     if(drivers.empty())
848         ui->backendCheckBox->setChecked(true);
849     else
850     {
851         if(drivers.size() == 1)
852             drivers = drivers[0].split(QChar(','));
853         for(QString &name : drivers)
854         {
855             name = name.trimmed();
856             /* Convert "mmdevapi" references to "wasapi" for backwards
857              * compatibility.
858              */
859             if(name == "-mmdevapi")
860                 name = "-wasapi";
861             else if(name == "mmdevapi")
862                 name = "wasapi";
863         }
864
865         bool lastWasEmpty = false;
866         foreach(const QString &backend, drivers)
867         {
868             lastWasEmpty = backend.isEmpty();
869             if(lastWasEmpty) continue;
870
871             if(!backend.startsWith(QChar('-')))
872                 for(int j = 0;backendList[j].backend_name[0];j++)
873                 {
874                     if(backend == backendList[j].backend_name)
875                     {
876                         ui->enabledBackendList->addItem(backendList[j].full_string);
877                         break;
878                     }
879                 }
880             else if(backend.size() > 1)
881             {
882                 QStringRef backendref{backend.rightRef(backend.size()-1)};
883                 for(int j = 0;backendList[j].backend_name[0];j++)
884                 {
885                     if(backendref == backendList[j].backend_name)
886                     {
887                         ui->disabledBackendList->addItem(backendList[j].full_string);
888                         break;
889                     }
890                 }
891             }
892         }
893         ui->backendCheckBox->setChecked(lastWasEmpty);
894     }
895
896     QString defaultreverb{settings.value("default-reverb").toString().toLower()};
897     ui->defaultReverbComboBox->setCurrentIndex(0);
898     if(defaultreverb.isEmpty() == false)
899     {
900         for(int i = 0;i < ui->defaultReverbComboBox->count();i++)
901         {
902             if(defaultreverb.compare(ui->defaultReverbComboBox->itemText(i).toLower()) == 0)
903             {
904                 ui->defaultReverbComboBox->setCurrentIndex(i);
905                 break;
906             }
907         }
908     }
909
910     QStringList excludefx{settings.value("excludefx").toStringList()};
911     if(excludefx.size() == 1)
912         excludefx = excludefx[0].split(QChar(','));
913     for(QString &name : excludefx)
914         name = name.trimmed();
915     ui->enableEaxReverbCheck->setChecked(!excludefx.contains("eaxreverb", Qt::CaseInsensitive));
916     ui->enableStdReverbCheck->setChecked(!excludefx.contains("reverb", Qt::CaseInsensitive));
917     ui->enableAutowahCheck->setChecked(!excludefx.contains("autowah", Qt::CaseInsensitive));
918     ui->enableChorusCheck->setChecked(!excludefx.contains("chorus", Qt::CaseInsensitive));
919     ui->enableCompressorCheck->setChecked(!excludefx.contains("compressor", Qt::CaseInsensitive));
920     ui->enableDistortionCheck->setChecked(!excludefx.contains("distortion", Qt::CaseInsensitive));
921     ui->enableEchoCheck->setChecked(!excludefx.contains("echo", Qt::CaseInsensitive));
922     ui->enableEqualizerCheck->setChecked(!excludefx.contains("equalizer", Qt::CaseInsensitive));
923     ui->enableFlangerCheck->setChecked(!excludefx.contains("flanger", Qt::CaseInsensitive));
924     ui->enableFrequencyShifterCheck->setChecked(!excludefx.contains("fshifter", Qt::CaseInsensitive));
925     ui->enableModulatorCheck->setChecked(!excludefx.contains("modulator", Qt::CaseInsensitive));
926     ui->enableDedicatedCheck->setChecked(!excludefx.contains("dedicated", Qt::CaseInsensitive));
927     ui->enablePitchShifterCheck->setChecked(!excludefx.contains("pshifter", Qt::CaseInsensitive));
928     ui->enableVocalMorpherCheck->setChecked(!excludefx.contains("vmorpher", Qt::CaseInsensitive));
929     if(ui->enableEaxCheck->isEnabled())
930         ui->enableEaxCheck->setChecked(getCheckState(settings.value("eax/enable")) != Qt::Unchecked);
931
932     ui->pulseAutospawnCheckBox->setCheckState(getCheckState(settings.value("pulse/spawn-server")));
933     ui->pulseAllowMovesCheckBox->setCheckState(getCheckState(settings.value("pulse/allow-moves")));
934     ui->pulseFixRateCheckBox->setCheckState(getCheckState(settings.value("pulse/fix-rate")));
935     ui->pulseAdjLatencyCheckBox->setCheckState(getCheckState(settings.value("pulse/adjust-latency")));
936
937     ui->pwireAssumeAudioCheckBox->setCheckState(getCheckState(settings.value("pipewire/assume-audio")));
938     ui->pwireRtMixCheckBox->setCheckState(getCheckState(settings.value("pipewire/rt-mix")));
939
940     ui->wasapiResamplerCheckBox->setCheckState(getCheckState(settings.value("wasapi/allow-resampler")));
941
942     ui->jackAutospawnCheckBox->setCheckState(getCheckState(settings.value("jack/spawn-server")));
943     ui->jackConnectPortsCheckBox->setCheckState(getCheckState(settings.value("jack/connect-ports")));
944     ui->jackRtMixCheckBox->setCheckState(getCheckState(settings.value("jack/rt-mix")));
945     ui->jackBufferSizeLine->setText(settings.value("jack/buffer-size", QString()).toString());
946     updateJackBufferSizeSlider();
947
948     ui->alsaDefaultDeviceLine->setText(settings.value("alsa/device", QString()).toString());
949     ui->alsaDefaultCaptureLine->setText(settings.value("alsa/capture", QString()).toString());
950     ui->alsaResamplerCheckBox->setCheckState(getCheckState(settings.value("alsa/allow-resampler")));
951     ui->alsaMmapCheckBox->setCheckState(getCheckState(settings.value("alsa/mmap")));
952
953     ui->ossDefaultDeviceLine->setText(settings.value("oss/device", QString()).toString());
954     ui->ossDefaultCaptureLine->setText(settings.value("oss/capture", QString()).toString());
955
956     ui->solarisDefaultDeviceLine->setText(settings.value("solaris/device", QString()).toString());
957
958     ui->waveOutputLine->setText(settings.value("wave/file", QString()).toString());
959     ui->waveBFormatCheckBox->setChecked(settings.value("wave/bformat", false).toBool());
960
961     ui->applyButton->setEnabled(false);
962     ui->closeCancelButton->setText(tr("Close"));
963     mNeedsSave = false;
964 }
965
966 void MainWindow::saveCurrentConfig()
967 {
968     saveConfig(getDefaultConfigName());
969     ui->applyButton->setEnabled(false);
970     ui->closeCancelButton->setText(tr("Close"));
971     mNeedsSave = false;
972     QMessageBox::information(this, tr("Information"),
973         tr("Applications using OpenAL need to be restarted for changes to take effect."));
974 }
975
976 void MainWindow::saveConfigAsFile()
977 {
978     QString fname{QFileDialog::getOpenFileName(this, tr("Select Files"))};
979     if(fname.isEmpty() == false)
980     {
981         saveConfig(fname);
982         ui->applyButton->setEnabled(false);
983         mNeedsSave = false;
984     }
985 }
986
987 void MainWindow::saveConfig(const QString &fname) const
988 {
989     QSettings settings{fname, QSettings::IniFormat};
990
991     /* HACK: Compound any stringlist values into a comma-separated string. */
992     QStringList allkeys{settings.allKeys()};
993     foreach(const QString &key, allkeys)
994     {
995         QStringList vals{settings.value(key).toStringList()};
996         if(vals.size() > 1)
997             settings.setValue(key, vals.join(QChar(',')));
998     }
999
1000     settings.setValue("sample-type", getValueFromName(sampleTypeList, ui->sampleFormatCombo->currentText()));
1001     settings.setValue("channels", getValueFromName(speakerModeList, ui->channelConfigCombo->currentText()));
1002
1003     uint rate{ui->sampleRateCombo->currentText().toUInt()};
1004     if(rate <= 0)
1005         settings.setValue("frequency", QString{});
1006     else
1007         settings.setValue("frequency", rate);
1008
1009     settings.setValue("period_size", ui->periodSizeEdit->text());
1010     settings.setValue("periods", ui->periodCountEdit->text());
1011
1012     settings.setValue("sources", ui->srcCountLineEdit->text());
1013     settings.setValue("slots", ui->effectSlotLineEdit->text());
1014
1015     settings.setValue("resampler", resamplerList[ui->resamplerSlider->value()].value);
1016
1017     settings.setValue("stereo-mode", getValueFromName(stereoModeList, ui->stereoModeCombo->currentText()));
1018     settings.setValue("stereo-encoding", getValueFromName(stereoEncList, ui->stereoEncodingComboBox->currentText()));
1019     settings.setValue("ambi-format", getValueFromName(ambiFormatList, ui->ambiFormatComboBox->currentText()));
1020
1021     settings.setValue("output-limiter", getCheckValue(ui->outputLimiterCheckBox));
1022     settings.setValue("dither", getCheckValue(ui->outputDitherCheckBox));
1023
1024     settings.setValue("decoder/hq-mode", getCheckValue(ui->decoderHQModeCheckBox));
1025     settings.setValue("decoder/distance-comp", getCheckValue(ui->decoderDistCompCheckBox));
1026     settings.setValue("decoder/nfc", getCheckValue(ui->decoderNFEffectsCheckBox));
1027     double speakerdist{ui->decoderSpeakerDistSpinBox->value()};
1028     settings.setValue("decoder/speaker-dist",
1029         (speakerdist != 1.0) ? QString::number(speakerdist) : QString{}
1030     );
1031
1032     settings.setValue("decoder/quad", ui->decoderQuadLineEdit->text());
1033     settings.setValue("decoder/surround51", ui->decoder51LineEdit->text());
1034     settings.setValue("decoder/surround61", ui->decoder61LineEdit->text());
1035     settings.setValue("decoder/surround71", ui->decoder71LineEdit->text());
1036     settings.setValue("decoder/surround3d71", ui->decoder3D71LineEdit->text());
1037
1038     QStringList strlist;
1039     if(!ui->enableSSECheckBox->isChecked())
1040         strlist.append("sse");
1041     if(!ui->enableSSE2CheckBox->isChecked())
1042         strlist.append("sse2");
1043     if(!ui->enableSSE3CheckBox->isChecked())
1044         strlist.append("sse3");
1045     if(!ui->enableSSE41CheckBox->isChecked())
1046         strlist.append("sse4.1");
1047     if(!ui->enableNeonCheckBox->isChecked())
1048         strlist.append("neon");
1049     settings.setValue("disable-cpu-exts", strlist.join(QChar(',')));
1050
1051     settings.setValue("hrtf-mode", hrtfModeList[ui->hrtfmodeSlider->value()].value);
1052
1053     if(ui->preferredHrtfComboBox->currentIndex() == 0)
1054         settings.setValue("default-hrtf", QString{});
1055     else
1056     {
1057         QString str{ui->preferredHrtfComboBox->currentText()};
1058         settings.setValue("default-hrtf", str);
1059     }
1060
1061     strlist.clear();
1062     strlist.reserve(ui->hrtfFileList->count());
1063     for(int i = 0;i < ui->hrtfFileList->count();i++)
1064         strlist.append(ui->hrtfFileList->item(i)->text());
1065     if(!strlist.empty() && ui->defaultHrtfPathsCheckBox->isChecked())
1066         strlist.append(QString{});
1067     settings.setValue("hrtf-paths", strlist.join(QChar{','}));
1068
1069     strlist.clear();
1070     for(int i = 0;i < ui->enabledBackendList->count();i++)
1071     {
1072         QString label{ui->enabledBackendList->item(i)->text()};
1073         for(int j = 0;backendList[j].backend_name[0];j++)
1074         {
1075             if(label == backendList[j].full_string)
1076             {
1077                 strlist.append(backendList[j].backend_name);
1078                 break;
1079             }
1080         }
1081     }
1082     for(int i = 0;i < ui->disabledBackendList->count();i++)
1083     {
1084         QString label{ui->disabledBackendList->item(i)->text()};
1085         for(int j = 0;backendList[j].backend_name[0];j++)
1086         {
1087             if(label == backendList[j].full_string)
1088             {
1089                 strlist.append(QChar{'-'}+QString{backendList[j].backend_name});
1090                 break;
1091             }
1092         }
1093     }
1094     if(strlist.empty() && !ui->backendCheckBox->isChecked())
1095         strlist.append("-all");
1096     else if(ui->backendCheckBox->isChecked())
1097         strlist.append(QString{});
1098     settings.setValue("drivers", strlist.join(QChar(',')));
1099
1100     // TODO: Remove check when we can properly match global values.
1101     if(ui->defaultReverbComboBox->currentIndex() == 0)
1102         settings.setValue("default-reverb", QString{});
1103     else
1104     {
1105         QString str{ui->defaultReverbComboBox->currentText().toLower()};
1106         settings.setValue("default-reverb", str);
1107     }
1108
1109     strlist.clear();
1110     if(!ui->enableEaxReverbCheck->isChecked())
1111         strlist.append("eaxreverb");
1112     if(!ui->enableStdReverbCheck->isChecked())
1113         strlist.append("reverb");
1114     if(!ui->enableAutowahCheck->isChecked())
1115         strlist.append("autowah");
1116     if(!ui->enableChorusCheck->isChecked())
1117         strlist.append("chorus");
1118     if(!ui->enableDistortionCheck->isChecked())
1119         strlist.append("distortion");
1120     if(!ui->enableCompressorCheck->isChecked())
1121         strlist.append("compressor");
1122     if(!ui->enableEchoCheck->isChecked())
1123         strlist.append("echo");
1124     if(!ui->enableEqualizerCheck->isChecked())
1125         strlist.append("equalizer");
1126     if(!ui->enableFlangerCheck->isChecked())
1127         strlist.append("flanger");
1128     if(!ui->enableFrequencyShifterCheck->isChecked())
1129         strlist.append("fshifter");
1130     if(!ui->enableModulatorCheck->isChecked())
1131         strlist.append("modulator");
1132     if(!ui->enableDedicatedCheck->isChecked())
1133         strlist.append("dedicated");
1134     if(!ui->enablePitchShifterCheck->isChecked())
1135         strlist.append("pshifter");
1136     if(!ui->enableVocalMorpherCheck->isChecked())
1137         strlist.append("vmorpher");
1138     settings.setValue("excludefx", strlist.join(QChar{','}));
1139     settings.setValue("eax/enable",
1140         (!ui->enableEaxCheck->isEnabled() || ui->enableEaxCheck->isChecked())
1141         ? QString{/*"true"*/} : QString{"false"});
1142
1143     settings.setValue("pipewire/assume-audio", getCheckValue(ui->pwireAssumeAudioCheckBox));
1144     settings.setValue("pipewire/rt-mix", getCheckValue(ui->pwireRtMixCheckBox));
1145
1146     settings.setValue("wasapi/allow-resampler", getCheckValue(ui->wasapiResamplerCheckBox));
1147
1148     settings.setValue("pulse/spawn-server", getCheckValue(ui->pulseAutospawnCheckBox));
1149     settings.setValue("pulse/allow-moves", getCheckValue(ui->pulseAllowMovesCheckBox));
1150     settings.setValue("pulse/fix-rate", getCheckValue(ui->pulseFixRateCheckBox));
1151     settings.setValue("pulse/adjust-latency", getCheckValue(ui->pulseAdjLatencyCheckBox));
1152
1153     settings.setValue("jack/spawn-server", getCheckValue(ui->jackAutospawnCheckBox));
1154     settings.setValue("jack/connect-ports", getCheckValue(ui->jackConnectPortsCheckBox));
1155     settings.setValue("jack/rt-mix", getCheckValue(ui->jackRtMixCheckBox));
1156     settings.setValue("jack/buffer-size", ui->jackBufferSizeLine->text());
1157
1158     settings.setValue("alsa/device", ui->alsaDefaultDeviceLine->text());
1159     settings.setValue("alsa/capture", ui->alsaDefaultCaptureLine->text());
1160     settings.setValue("alsa/allow-resampler", getCheckValue(ui->alsaResamplerCheckBox));
1161     settings.setValue("alsa/mmap", getCheckValue(ui->alsaMmapCheckBox));
1162
1163     settings.setValue("oss/device", ui->ossDefaultDeviceLine->text());
1164     settings.setValue("oss/capture", ui->ossDefaultCaptureLine->text());
1165
1166     settings.setValue("solaris/device", ui->solarisDefaultDeviceLine->text());
1167
1168     settings.setValue("wave/file", ui->waveOutputLine->text());
1169     settings.setValue("wave/bformat",
1170         ui->waveBFormatCheckBox->isChecked() ? QString{"true"} : QString{/*"false"*/}
1171     );
1172
1173     /* Remove empty keys
1174      * FIXME: Should only remove keys whose value matches the globally-specified value.
1175      */
1176     allkeys = settings.allKeys();
1177     foreach(const QString &key, allkeys)
1178     {
1179         QString str{settings.value(key).toString()};
1180         if(str == QString{})
1181             settings.remove(key);
1182     }
1183 }
1184
1185
1186 void MainWindow::enableApplyButton()
1187 {
1188     if(!mNeedsSave)
1189         ui->applyButton->setEnabled(true);
1190     mNeedsSave = true;
1191     ui->closeCancelButton->setText(tr("Cancel"));
1192 }
1193
1194
1195 void MainWindow::updateResamplerLabel(int num)
1196 {
1197     ui->resamplerLabel->setText(resamplerList[num].name);
1198     enableApplyButton();
1199 }
1200
1201
1202 void MainWindow::updatePeriodSizeEdit(int size)
1203 {
1204     ui->periodSizeEdit->clear();
1205     if(size >= 64)
1206         ui->periodSizeEdit->insert(QString::number(size));
1207     enableApplyButton();
1208 }
1209
1210 void MainWindow::updatePeriodSizeSlider()
1211 {
1212     int pos = ui->periodSizeEdit->text().toInt();
1213     if(pos >= 64)
1214     {
1215         if(pos > 8192)
1216             pos = 8192;
1217         ui->periodSizeSlider->setSliderPosition(pos);
1218     }
1219     enableApplyButton();
1220 }
1221
1222 void MainWindow::updatePeriodCountEdit(int count)
1223 {
1224     ui->periodCountEdit->clear();
1225     if(count >= 2)
1226         ui->periodCountEdit->insert(QString::number(count));
1227     enableApplyButton();
1228 }
1229
1230 void MainWindow::updatePeriodCountSlider()
1231 {
1232     int pos = ui->periodCountEdit->text().toInt();
1233     if(pos < 2)
1234         pos = 0;
1235     else if(pos > 16)
1236         pos = 16;
1237     ui->periodCountSlider->setSliderPosition(pos);
1238     enableApplyButton();
1239 }
1240
1241
1242 void MainWindow::selectQuadDecoderFile()
1243 { selectDecoderFile(ui->decoderQuadLineEdit, "Select Quadraphonic Decoder");}
1244 void MainWindow::select51DecoderFile()
1245 { selectDecoderFile(ui->decoder51LineEdit, "Select 5.1 Surround Decoder");}
1246 void MainWindow::select61DecoderFile()
1247 { selectDecoderFile(ui->decoder61LineEdit, "Select 6.1 Surround Decoder");}
1248 void MainWindow::select71DecoderFile()
1249 { selectDecoderFile(ui->decoder71LineEdit, "Select 7.1 Surround Decoder");}
1250 void MainWindow::select3D71DecoderFile()
1251 { selectDecoderFile(ui->decoder3D71LineEdit, "Select 3D7.1 Surround Decoder");}
1252 void MainWindow::selectDecoderFile(QLineEdit *line, const char *caption)
1253 {
1254     QString dir{line->text()};
1255     if(dir.isEmpty() || QDir::isRelativePath(dir))
1256     {
1257         QStringList paths{getAllDataPaths("/openal/presets")};
1258         while(!paths.isEmpty())
1259         {
1260             if(QDir{paths.last()}.exists())
1261             {
1262                 dir = paths.last();
1263                 break;
1264             }
1265             paths.removeLast();
1266         }
1267     }
1268     QString fname{QFileDialog::getOpenFileName(this, tr(caption),
1269         dir, tr("AmbDec Files (*.ambdec);;All Files (*.*)"))};
1270     if(!fname.isEmpty())
1271     {
1272         line->setText(fname);
1273         enableApplyButton();
1274     }
1275 }
1276
1277
1278 void MainWindow::updateJackBufferSizeEdit(int size)
1279 {
1280     ui->jackBufferSizeLine->clear();
1281     if(size > 0)
1282         ui->jackBufferSizeLine->insert(QString::number(1<<size));
1283     enableApplyButton();
1284 }
1285
1286 void MainWindow::updateJackBufferSizeSlider()
1287 {
1288     int value{ui->jackBufferSizeLine->text().toInt()};
1289     auto pos = static_cast<int>(floor(log2(value) + 0.5));
1290     ui->jackBufferSizeSlider->setSliderPosition(pos);
1291     enableApplyButton();
1292 }
1293
1294
1295 void MainWindow::updateHrtfModeLabel(int num)
1296 {
1297     ui->hrtfmodeLabel->setText(hrtfModeList[num].name);
1298     enableApplyButton();
1299 }
1300
1301
1302 void MainWindow::addHrtfFile()
1303 {
1304     QString path{QFileDialog::getExistingDirectory(this, tr("Select HRTF Path"))};
1305     if(path.isEmpty() == false && !getAllDataPaths("/openal/hrtf").contains(path))
1306     {
1307         ui->hrtfFileList->addItem(path);
1308         enableApplyButton();
1309     }
1310 }
1311
1312 void MainWindow::removeHrtfFile()
1313 {
1314     QList<QListWidgetItem*> selected{ui->hrtfFileList->selectedItems()};
1315     if(!selected.isEmpty())
1316     {
1317         foreach(QListWidgetItem *item, selected)
1318             delete item;
1319         enableApplyButton();
1320     }
1321 }
1322
1323 void MainWindow::updateHrtfRemoveButton()
1324 {
1325     ui->hrtfRemoveButton->setEnabled(!ui->hrtfFileList->selectedItems().empty());
1326 }
1327
1328 void MainWindow::showEnabledBackendMenu(QPoint pt)
1329 {
1330     QHash<QAction*,QString> actionMap;
1331
1332     pt = ui->enabledBackendList->mapToGlobal(pt);
1333
1334     QMenu ctxmenu;
1335     QAction *removeAction{ctxmenu.addAction(QIcon::fromTheme("list-remove"), "Remove")};
1336     if(ui->enabledBackendList->selectedItems().empty())
1337         removeAction->setEnabled(false);
1338     ctxmenu.addSeparator();
1339     for(size_t i = 0;backendList[i].backend_name[0];i++)
1340     {
1341         QString backend{backendList[i].full_string};
1342         QAction *action{ctxmenu.addAction(QString("Add ")+backend)};
1343         actionMap[action] = backend;
1344         if(!ui->enabledBackendList->findItems(backend, Qt::MatchFixedString).empty() ||
1345            !ui->disabledBackendList->findItems(backend, Qt::MatchFixedString).empty())
1346             action->setEnabled(false);
1347     }
1348
1349     QAction *gotAction{ctxmenu.exec(pt)};
1350     if(gotAction == removeAction)
1351     {
1352         QList<QListWidgetItem*> selected{ui->enabledBackendList->selectedItems()};
1353         foreach(QListWidgetItem *item, selected)
1354             delete item;
1355         enableApplyButton();
1356     }
1357     else if(gotAction != nullptr)
1358     {
1359         auto iter = actionMap.constFind(gotAction);
1360         if(iter != actionMap.cend())
1361             ui->enabledBackendList->addItem(iter.value());
1362         enableApplyButton();
1363     }
1364 }
1365
1366 void MainWindow::showDisabledBackendMenu(QPoint pt)
1367 {
1368     QHash<QAction*,QString> actionMap;
1369
1370     pt = ui->disabledBackendList->mapToGlobal(pt);
1371
1372     QMenu ctxmenu;
1373     QAction *removeAction{ctxmenu.addAction(QIcon::fromTheme("list-remove"), "Remove")};
1374     if(ui->disabledBackendList->selectedItems().empty())
1375         removeAction->setEnabled(false);
1376     ctxmenu.addSeparator();
1377     for(size_t i = 0;backendList[i].backend_name[0];i++)
1378     {
1379         QString backend{backendList[i].full_string};
1380         QAction *action{ctxmenu.addAction(QString("Add ")+backend)};
1381         actionMap[action] = backend;
1382         if(!ui->disabledBackendList->findItems(backend, Qt::MatchFixedString).empty() ||
1383            !ui->enabledBackendList->findItems(backend, Qt::MatchFixedString).empty())
1384             action->setEnabled(false);
1385     }
1386
1387     QAction *gotAction{ctxmenu.exec(pt)};
1388     if(gotAction == removeAction)
1389     {
1390         QList<QListWidgetItem*> selected{ui->disabledBackendList->selectedItems()};
1391         foreach(QListWidgetItem *item, selected)
1392             delete item;
1393         enableApplyButton();
1394     }
1395     else if(gotAction != nullptr)
1396     {
1397         auto iter = actionMap.constFind(gotAction);
1398         if(iter != actionMap.cend())
1399             ui->disabledBackendList->addItem(iter.value());
1400         enableApplyButton();
1401     }
1402 }
1403
1404 void MainWindow::selectOSSPlayback()
1405 {
1406     QString current{ui->ossDefaultDeviceLine->text()};
1407     if(current.isEmpty()) current = ui->ossDefaultDeviceLine->placeholderText();
1408     QString fname{QFileDialog::getOpenFileName(this, tr("Select Playback Device"), current)};
1409     if(!fname.isEmpty())
1410     {
1411         ui->ossDefaultDeviceLine->setText(fname);
1412         enableApplyButton();
1413     }
1414 }
1415
1416 void MainWindow::selectOSSCapture()
1417 {
1418     QString current{ui->ossDefaultCaptureLine->text()};
1419     if(current.isEmpty()) current = ui->ossDefaultCaptureLine->placeholderText();
1420     QString fname{QFileDialog::getOpenFileName(this, tr("Select Capture Device"), current)};
1421     if(!fname.isEmpty())
1422     {
1423         ui->ossDefaultCaptureLine->setText(fname);
1424         enableApplyButton();
1425     }
1426 }
1427
1428 void MainWindow::selectSolarisPlayback()
1429 {
1430     QString current{ui->solarisDefaultDeviceLine->text()};
1431     if(current.isEmpty()) current = ui->solarisDefaultDeviceLine->placeholderText();
1432     QString fname{QFileDialog::getOpenFileName(this, tr("Select Playback Device"), current)};
1433     if(!fname.isEmpty())
1434     {
1435         ui->solarisDefaultDeviceLine->setText(fname);
1436         enableApplyButton();
1437     }
1438 }
1439
1440 void MainWindow::selectWaveOutput()
1441 {
1442     QString fname{QFileDialog::getSaveFileName(this, tr("Select Wave File Output"),
1443         ui->waveOutputLine->text(), tr("Wave Files (*.wav *.amb);;All Files (*.*)"))};
1444     if(!fname.isEmpty())
1445     {
1446         ui->waveOutputLine->setText(fname);
1447         enableApplyButton();
1448     }
1449 }