1 #include <QRegularExpression>
2 
3 #include "soundio/soundmanagerconfig.h"
4 
5 #include "soundio/soundmanagerutil.h"
6 #include "soundio/sounddevice.h"
7 #include "soundio/soundmanager.h"
8 #include "util/cmdlineargs.h"
9 #include "util/math.h"
10 
11 // this (7) represents latency values from 1 ms to about 80 ms -- bkgood
12 const unsigned int SoundManagerConfig::kMaxAudioBufferSizeIndex = 7;
13 
14 const QString SoundManagerConfig::kDefaultAPI = QStringLiteral("None");
15 const QString SoundManagerConfig::kEmptyComboBox = QStringLiteral("---");
16 // Sample Rate even the cheap sound Devices will support most likely
17 const unsigned int SoundManagerConfig::kFallbackSampleRate = 48000;
18 const unsigned int SoundManagerConfig::kDefaultDeckCount = 2;
19 // audioBufferSizeIndex=5 means about 21 ms of latency which is default in trunk r2453 -- bkgood
20 const int SoundManagerConfig::kDefaultAudioBufferSizeIndex = 5;
21 
22 const int SoundManagerConfig::kDefaultSyncBuffers = 2;
23 
24 namespace {
25 const QString xmlRootElement = "SoundManagerConfig";
26 const QString xmlAttributeApi = "api";
27 const QString xmlAttributeSampleRate = "samplerate";
28 const QString xmlAttributeBufferSize = "latency";
29 const QString xmlAttributeSyncBuffers = "sync_buffers";
30 const QString xmlAttributeForceNetworkClock = "force_network_clock";
31 const QString xmlAttributeDeckCount = "deck_count";
32 
33 const QString xmlElementSoundDevice = "SoundDevice";
34 const QString xmlAttributeDeviceName = "name";
35 const QString xmlAttributeAlsaHwDevice = "alsaHwDevice";
36 const QString xmlAttributePortAudioIndex = "portAudioIndex";
37 
38 const QString xmlElementOutput = "output";
39 const QString xmlElementInput = "input";
40 
41 const QRegularExpression kLegacyFormatRegex("((\\d*), )(.*) \\((plug)?(hw:(\\d)+(,(\\d)+))?\\)");
42 } // namespace
43 
SoundManagerConfig(SoundManager * pSoundManager)44 SoundManagerConfig::SoundManagerConfig(SoundManager* pSoundManager)
45     : m_api(kDefaultAPI),
46       m_sampleRate(kFallbackSampleRate),
47       m_deckCount(kDefaultDeckCount),
48       m_audioBufferSizeIndex(kDefaultAudioBufferSizeIndex),
49       m_syncBuffers(2),
50       m_forceNetworkClock(false),
51       m_iNumMicInputs(0),
52       m_bExternalRecordBroadcastConnected(false),
53       m_pSoundManager(pSoundManager) {
54     m_configFile = QFileInfo(QDir(CmdlineArgs::Instance().getSettingsPath()).filePath(SOUNDMANAGERCONFIG_FILENAME));
55 }
56 
~SoundManagerConfig()57 SoundManagerConfig::~SoundManagerConfig() {
58     // don't write to disk here, it's SoundManager's responsibility
59     // to save its own configuration -- bkgood
60 }
61 
62 /**
63  * Read the SoundManagerConfig xml serialization at the predetermined
64  * path
65  * @returns false if the file can't be read or is invalid XML, true otherwise
66  */
readFromDisk()67 bool SoundManagerConfig::readFromDisk() {
68     QFile file(m_configFile.absoluteFilePath());
69     QDomDocument doc;
70     QDomElement rootElement;
71     if (!file.open(QIODevice::ReadOnly)) {
72         return false;
73     }
74     if (!doc.setContent(&file)) {
75         file.close();
76         return false;
77     }
78     file.close();
79     rootElement = doc.documentElement();
80     setAPI(rootElement.attribute(xmlAttributeApi));
81     setSampleRate(rootElement.attribute(xmlAttributeSampleRate, "0").toUInt());
82     // audioBufferSizeIndex is refereed as "latency" in the config file
83     setAudioBufferSizeIndex(rootElement.attribute(xmlAttributeBufferSize, "0").toUInt());
84     setSyncBuffers(rootElement.attribute(xmlAttributeSyncBuffers, "2").toUInt());
85     setForceNetworkClock(rootElement.attribute(xmlAttributeForceNetworkClock,
86             "0").toUInt() != 0);
87     setDeckCount(rootElement.attribute(xmlAttributeDeckCount,
88             QString(kDefaultDeckCount)).toUInt());
89     clearOutputs();
90     clearInputs();
91     QDomNodeList devElements(rootElement.elementsByTagName(xmlElementSoundDevice));
92 
93     VERIFY_OR_DEBUG_ASSERT(m_pSoundManager != nullptr) {
94         return false;
95     }
96     QList<SoundDevicePointer> soundDevices = m_pSoundManager->getDeviceList(m_api, true, true);
97 
98     for (int i = 0; i < devElements.count(); ++i) {
99         QDomElement devElement(devElements.at(i).toElement());
100         if (devElement.isNull()) {
101             continue;
102         }
103         SoundDeviceId deviceIdFromFile;
104         deviceIdFromFile.name = devElement.attribute(xmlAttributeDeviceName);
105         if (deviceIdFromFile.name.isEmpty()) {
106             continue;
107         }
108 
109         // TODO: remove this ugly hack after Mixxx 2.2.3 is released
110         QRegularExpressionMatch match = kLegacyFormatRegex.match(deviceIdFromFile.name);
111         if (match.hasMatch()) {
112             deviceIdFromFile.name = match.captured(3);
113             deviceIdFromFile.alsaHwDevice = match.captured(5);
114             deviceIdFromFile.portAudioIndex = match.captured(2).toInt();
115         } else {
116             deviceIdFromFile.alsaHwDevice = devElement.attribute(xmlAttributeAlsaHwDevice);
117             deviceIdFromFile.portAudioIndex = devElement.attribute(xmlAttributePortAudioIndex).toInt();
118         }
119 
120         int devicesMatchingByName = 0;
121         for (const auto& soundDevice : soundDevices) {
122             SoundDeviceId hardwareDeviceId = soundDevice->getDeviceId();
123             if (hardwareDeviceId.name == deviceIdFromFile.name) {
124                 devicesMatchingByName++;
125             }
126         }
127 
128         QDomNodeList outElements(devElement.elementsByTagName(xmlElementOutput));
129         QDomNodeList inElements(devElement.elementsByTagName(xmlElementInput));
130 
131         if (devicesMatchingByName == 0) {
132             continue;
133         } else if (devicesMatchingByName == 1) {
134             // There is only one device with this name, so it is unambiguous
135             // which it is. Neither the alsaHwDevice nor portAudioIndex are
136             // very reliable as persistent identifiers across restarts of Mixxx.
137             // Set deviceIdFromFile's alsaHwDevice and portAudioIndex to match
138             // the hardwareDeviceId so operator== works for SoundDeviceId.
139             for (const auto& soundDevice : soundDevices) {
140                 SoundDeviceId hardwareDeviceId = soundDevice->getDeviceId();
141                 if (hardwareDeviceId.name == deviceIdFromFile.name) {
142                     deviceIdFromFile.alsaHwDevice = hardwareDeviceId.alsaHwDevice;
143                     deviceIdFromFile.portAudioIndex = hardwareDeviceId.portAudioIndex;
144                 }
145             }
146         } else {
147             // It is not clear which hardwareDeviceId corresponds to the device
148             // listed in the configuration file using only the name.
149             if (!deviceIdFromFile.alsaHwDevice.isEmpty()) {
150                 // If using ALSA, attempt to match based on the ALSA device name.
151                 // This is reliable between restarts of Mixxx until the user
152                 // unplugs an audio interface or restarts Linux.
153                 // NOTE(Be): I am not sure if there is a way to assign a
154                 // persistent ALSA device name across restarts of Linux for
155                 // multiple devices with the same name. This might be possible
156                 // somehow with a udev rule matching device serial numbers, but
157                 // I have not tested this.
158                 for (const auto& soundDevice : soundDevices) {
159                     SoundDeviceId hardwareDeviceId = soundDevice->getDeviceId();
160                     if (hardwareDeviceId.name == deviceIdFromFile.name
161                             && hardwareDeviceId.alsaHwDevice == deviceIdFromFile.alsaHwDevice) {
162                         deviceIdFromFile.portAudioIndex = hardwareDeviceId.portAudioIndex;
163                         break;
164                     }
165                 }
166             } else {
167                 // Check if the one of the matching devices has the configured in and output channels
168                 for (const auto& soundDevice : soundDevices) {
169                     SoundDeviceId hardwareDeviceId = soundDevice->getDeviceId();
170                     if (hardwareDeviceId.name == deviceIdFromFile.name &&
171                             soundDevice->getNumOutputChannels() >=
172                                     outElements.count() &&
173                             soundDevice->getNumInputChannels() >=
174                                     inElements.count()) {
175                         deviceIdFromFile.portAudioIndex = hardwareDeviceId.portAudioIndex;
176                         break;
177                     }
178                 }
179             }
180         }
181 
182         for (int j = 0; j < outElements.count(); ++j) {
183             QDomElement outElement(outElements.at(j).toElement());
184             if (outElement.isNull()) {
185                 continue;
186             }
187             AudioOutput out(AudioOutput::fromXML(outElement));
188             if (out.getType() == AudioPath::INVALID) {
189                 continue;
190             }
191             bool dupe(false);
192             for (const AudioOutput& otherOut : qAsConst(m_outputs)) {
193                 if (out == otherOut
194                         && out.getChannelGroup() == otherOut.getChannelGroup()) {
195                     dupe = true;
196                     break;
197                 }
198             }
199             if (dupe) {
200                 continue;
201             }
202 
203             addOutput(deviceIdFromFile, out);
204         }
205         for (int j = 0; j < inElements.count(); ++j) {
206             QDomElement inElement(inElements.at(j).toElement());
207             if (inElement.isNull()) {
208                 continue;
209             }
210             AudioInput in(AudioInput::fromXML(inElement));
211             if (in.getType() == AudioPath::INVALID) {
212                 continue;
213             }
214             bool dupe(false);
215             for (const AudioInput& otherIn : qAsConst(m_inputs)) {
216                 if (in == otherIn
217                         && in.getChannelGroup() == otherIn.getChannelGroup()) {
218                     dupe = true;
219                     break;
220                 }
221             }
222             if (dupe) {
223                 continue;
224             }
225             addInput(deviceIdFromFile, in);
226         }
227     }
228     return true;
229 }
230 
writeToDisk() const231 bool SoundManagerConfig::writeToDisk() const {
232     QDomDocument doc(xmlRootElement);
233     QDomElement docElement(doc.createElement(xmlRootElement));
234     docElement.setAttribute(xmlAttributeApi, m_api);
235     docElement.setAttribute(xmlAttributeSampleRate, m_sampleRate);
236     docElement.setAttribute(xmlAttributeBufferSize, m_audioBufferSizeIndex);
237     docElement.setAttribute(xmlAttributeSyncBuffers, m_syncBuffers);
238     docElement.setAttribute(xmlAttributeForceNetworkClock, m_forceNetworkClock);
239     docElement.setAttribute(xmlAttributeDeckCount, m_deckCount);
240     doc.appendChild(docElement);
241 
242     const QSet<SoundDeviceId> deviceIds = getDevices();
243     for (const auto& deviceId : deviceIds) {
244         QDomElement devElement(doc.createElement(xmlElementSoundDevice));
245         devElement.setAttribute(xmlAttributeDeviceName, deviceId.name);
246         devElement.setAttribute(xmlAttributePortAudioIndex, deviceId.portAudioIndex);
247         if (m_api == MIXXX_PORTAUDIO_ALSA_STRING) {
248             devElement.setAttribute(xmlAttributeAlsaHwDevice, deviceId.alsaHwDevice);
249         }
250 
251         for (auto it = m_inputs.constFind(deviceId);
252                 it != m_inputs.constEnd() && it.key() == deviceId;
253                 ++it) {
254             QDomElement inElement(doc.createElement(xmlElementInput));
255             it.value().toXML(&inElement);
256             devElement.appendChild(inElement);
257         }
258 
259         for (auto it = m_outputs.constFind(deviceId);
260                 it != m_outputs.constEnd() && it.key() == deviceId;
261                 ++it) {
262             QDomElement outElement(doc.createElement(xmlElementOutput));
263             it.value().toXML(&outElement);
264             devElement.appendChild(outElement);
265         }
266         docElement.appendChild(devElement);
267     }
268 
269     QFile file(m_configFile.absoluteFilePath());
270     if (!file.open(QIODevice::Truncate | QIODevice::WriteOnly)) {
271         return false;
272     }
273     file.write(doc.toString().toUtf8());
274     file.close();
275     return true;
276 }
277 
getAPI() const278 QString SoundManagerConfig::getAPI() const {
279     return m_api;
280 }
281 
setAPI(const QString & api)282 void SoundManagerConfig::setAPI(const QString &api) {
283     // SoundManagerConfig doesn't necessarily have access to a SoundManager
284     // instance, so I can't check for input validity here -- bkgood
285     m_api = api;
286 }
287 
288 /**
289  * Checks that the API in the object is valid according to the list of APIs
290  * given by SoundManager.
291  * @returns false if the API is not found in SoundManager's list, otherwise
292  *          true
293  */
checkAPI()294 bool SoundManagerConfig::checkAPI() {
295     VERIFY_OR_DEBUG_ASSERT(m_pSoundManager != nullptr) {
296         return false;
297     }
298     if (!m_pSoundManager->getHostAPIList().contains(m_api) && m_api != kDefaultAPI) {
299         return false;
300     }
301     return true;
302 }
303 
getSampleRate() const304 unsigned int SoundManagerConfig::getSampleRate() const {
305     return m_sampleRate;
306 }
307 
setSampleRate(unsigned int sampleRate)308 void SoundManagerConfig::setSampleRate(unsigned int sampleRate) {
309     // making sure we don't divide by zero elsewhere
310     m_sampleRate = sampleRate != 0 ? sampleRate : kFallbackSampleRate;
311 }
312 
313 
getSyncBuffers() const314 unsigned int SoundManagerConfig::getSyncBuffers() const {
315     return m_syncBuffers;
316 }
317 
setSyncBuffers(unsigned int syncBuffers)318 void SoundManagerConfig::setSyncBuffers(unsigned int syncBuffers) {
319     // making sure we don't divide by zero elsewhere
320     m_syncBuffers = qMin(syncBuffers, (unsigned int)2);
321 }
322 
getForceNetworkClock() const323 bool SoundManagerConfig::getForceNetworkClock() const {
324     return m_forceNetworkClock;
325 }
326 
setForceNetworkClock(bool force)327 void SoundManagerConfig::setForceNetworkClock(bool force) {
328     m_forceNetworkClock = force;
329 }
330 
331 /**
332  * Checks that the sample rate in the object is valid according to the list of
333  * sample rates given by SoundManager.
334  * @returns false if the sample rate is not found in SoundManager's list,
335  *          otherwise true
336  */
checkSampleRate(const SoundManager & soundManager)337 bool SoundManagerConfig::checkSampleRate(const SoundManager &soundManager) {
338     if (!soundManager.getSampleRates(m_api).contains(m_sampleRate)) {
339         return false;
340     }
341     return true;
342 }
343 
getDeckCount() const344 unsigned int SoundManagerConfig::getDeckCount() const {
345     return m_deckCount;
346 }
347 
setDeckCount(unsigned int deckCount)348 void SoundManagerConfig::setDeckCount(unsigned int deckCount) {
349     m_deckCount = deckCount;
350 }
351 
setCorrectDeckCount(int configuredDeckCount)352 void SoundManagerConfig::setCorrectDeckCount(int configuredDeckCount) {
353     int minimum_deck_count = 0;
354 
355     const QSet<SoundDeviceId> deviceIds = getDevices();
356     for (const auto& deviceId : deviceIds) {
357         for (auto it = m_inputs.constFind(deviceId);
358                 it != m_inputs.constEnd() && it.key() == deviceId;
359                 ++it) {
360             const int index = it.value().getIndex();
361             const AudioPathType type = it.value().getType();
362             if ((type == AudioInput::DECK ||
363                         type == AudioInput::VINYLCONTROL ||
364                         type == AudioInput::AUXILIARY) &&
365                     index + 1 > minimum_deck_count) {
366                 qDebug() << "Found an input connection above current deck count";
367                 minimum_deck_count = index + 1;
368             }
369         }
370         for (auto it = m_outputs.constFind(deviceId);
371                 it != m_outputs.constEnd() && it.key() == deviceId;
372                 ++it) {
373             const int index = it.value().getIndex();
374             const AudioPathType type = it.value().getType();
375             if (type == AudioOutput::DECK && index + 1 > minimum_deck_count) {
376                 qDebug() << "Found an output connection above current deck count";
377                 minimum_deck_count = index + 1;
378             }
379         }
380     }
381 
382     if (minimum_deck_count > configuredDeckCount) {
383         m_deckCount = minimum_deck_count;
384     } else {
385         m_deckCount = configuredDeckCount;
386     }
387 }
388 
getAudioBufferSizeIndex() const389 unsigned int SoundManagerConfig::getAudioBufferSizeIndex() const {
390     return m_audioBufferSizeIndex;
391 }
392 
393 // FIXME: This is incorrect when using JACK as the sound API!
394 // m_audioBufferSizeIndex does not reflect JACK's buffer size.
getFramesPerBuffer() const395 unsigned int SoundManagerConfig::getFramesPerBuffer() const {
396     // endless loop otherwise
397     unsigned int audioBufferSizeIndex = m_audioBufferSizeIndex;
398     VERIFY_OR_DEBUG_ASSERT(audioBufferSizeIndex > 0) {
399         audioBufferSizeIndex = kDefaultAudioBufferSizeIndex;
400     }
401     unsigned int framesPerBuffer = 1;
402     double sampleRate = m_sampleRate; // need this to avoid int division
403     // first, get to the framesPerBuffer value corresponding to latency index 1
404     for (; framesPerBuffer / sampleRate * 1000 < 1.0; framesPerBuffer *= 2) {
405     }
406     // then, keep going until we get to our desired latency index (if not 1)
407     for (unsigned int latencyIndex = 1; latencyIndex < audioBufferSizeIndex; ++latencyIndex) {
408         framesPerBuffer <<= 1; // *= 2
409     }
410     return framesPerBuffer;
411 }
412 
413 // FIXME: This is incorrect when using JACK as the sound API!
414 // m_audioBufferSizeIndex does not reflect JACK's buffer size.
getProcessingLatency() const415 double SoundManagerConfig::getProcessingLatency() const {
416     return static_cast<double>(getFramesPerBuffer()) / m_sampleRate * 1000.0;
417 }
418 
419 
420 // Set the audio buffer size
421 // @warning This IS NOT a value in milliseconds, or a number of frames per
422 // buffer. It is an index, where 1 is the first power-of-two buffer size (in
423 // frames) which corresponds to a latency greater than or equal to 1 ms, 2 is
424 // the second, etc. This is so that latency values are roughly equivalent
425 // between different sample rates.
setAudioBufferSizeIndex(unsigned int sizeIndex)426 void SoundManagerConfig::setAudioBufferSizeIndex(unsigned int sizeIndex) {
427     // latency should be either the min of kMaxAudioBufferSizeIndex and the passed value
428     // if it's 0, pretend it was 1 -- bkgood
429     m_audioBufferSizeIndex = sizeIndex != 0 ? math_min(sizeIndex, kMaxAudioBufferSizeIndex) : 1;
430 }
431 
addOutput(const SoundDeviceId & device,const AudioOutput & out)432 void SoundManagerConfig::addOutput(const SoundDeviceId &device, const AudioOutput &out) {
433     m_outputs.insert(device, out);
434 }
435 
addInput(const SoundDeviceId & device,const AudioInput & in)436 void SoundManagerConfig::addInput(const SoundDeviceId &device, const AudioInput &in) {
437     m_inputs.insert(device, in);
438     if (in.getType() == AudioPath::MICROPHONE) {
439         m_iNumMicInputs++;
440     } else if (in.getType() == AudioPath::RECORD_BROADCAST) {
441         m_bExternalRecordBroadcastConnected = true;
442     }
443 }
444 
getOutputs() const445 QMultiHash<SoundDeviceId, AudioOutput> SoundManagerConfig::getOutputs() const {
446     return m_outputs;
447 }
448 
getInputs() const449 QMultiHash<SoundDeviceId, AudioInput> SoundManagerConfig::getInputs() const {
450     return m_inputs;
451 }
452 
clearOutputs()453 void SoundManagerConfig::clearOutputs() {
454     m_outputs.clear();
455 }
456 
clearInputs()457 void SoundManagerConfig::clearInputs() {
458     m_inputs.clear();
459     m_iNumMicInputs = 0;
460     m_bExternalRecordBroadcastConnected = false;
461 }
462 
hasMicInputs()463 bool SoundManagerConfig::hasMicInputs() {
464     return m_iNumMicInputs;
465 }
466 
hasExternalRecordBroadcast()467 bool SoundManagerConfig::hasExternalRecordBroadcast() {
468     return m_bExternalRecordBroadcastConnected;
469 }
470 
471 /**
472  * Loads default values for API, master output, sample rate and/or latency.
473  * @param soundManager pointer to SoundManager instance to load data from
474  * @param flags Bitfield to determine which defaults to load, use something
475  *              like SoundManagerConfig::API | SoundManagerConfig::DEVICES to
476  *              load default API and master device.
477  */
loadDefaults(SoundManager * soundManager,unsigned int flags)478 void SoundManagerConfig::loadDefaults(SoundManager *soundManager, unsigned int flags) {
479     if (flags & SoundManagerConfig::API) {
480         QList<QString> apiList = soundManager->getHostAPIList();
481         if (!apiList.isEmpty()) {
482 #ifdef __LINUX__
483             //Check for JACK and use that if it's available, otherwise use ALSA
484             if (apiList.contains(MIXXX_PORTAUDIO_JACK_STRING)) {
485                 m_api = MIXXX_PORTAUDIO_JACK_STRING;
486             } else {
487                 m_api = MIXXX_PORTAUDIO_ALSA_STRING;
488             }
489 #endif
490 #ifdef __WINDOWS__
491             //Existence of ASIO doesn't necessarily mean you've got ASIO devices
492             //Do something more advanced one day if you like - Adam
493             // hoping this counts as more advanced, tests if ASIO is an option
494             // and then that we have at least one ASIO output device -- bkgood
495             if (apiList.contains(MIXXX_PORTAUDIO_ASIO_STRING)
496                    && !soundManager->getDeviceList(
497                        MIXXX_PORTAUDIO_ASIO_STRING, true, false).isEmpty()) {
498                 m_api = MIXXX_PORTAUDIO_ASIO_STRING;
499             } else {
500                 m_api = MIXXX_PORTAUDIO_DIRECTSOUND_STRING;
501             }
502 #endif
503 #ifdef __APPLE__
504             m_api = MIXXX_PORTAUDIO_COREAUDIO_STRING;
505 #endif
506         }
507     }
508 
509     unsigned int defaultSampleRate = kFallbackSampleRate;
510     if (flags & SoundManagerConfig::DEVICES) {
511         clearOutputs();
512         clearInputs();
513         QList<SoundDevicePointer> outputDevices = soundManager->getDeviceList(m_api, true, false);
514         if (!outputDevices.isEmpty()) {
515             for (const auto& pDevice: outputDevices) {
516                 if (pDevice->getNumOutputChannels() < 2) {
517                     continue;
518                 }
519                 AudioOutput masterOut(AudioPath::MASTER, 0, 2, 0);
520                 addOutput(pDevice->getDeviceId(), masterOut);
521                 defaultSampleRate = pDevice->getDefaultSampleRate();
522                 break;
523             }
524         }
525     }
526     if (flags & SoundManagerConfig::OTHER) {
527         QList<unsigned int> sampleRates = soundManager->getSampleRates(m_api);
528         if (sampleRates.contains(defaultSampleRate)) {
529             m_sampleRate = defaultSampleRate;
530         } else if (sampleRates.contains(kFallbackSampleRate)) {
531             m_sampleRate = kFallbackSampleRate;
532         } else if (!sampleRates.isEmpty()) {
533             m_sampleRate = sampleRates.first();
534         } else {
535             qWarning() << "got empty sample rate list from SoundManager, this is a bug";
536             m_sampleRate = kFallbackSampleRate;
537         }
538         m_audioBufferSizeIndex = kDefaultAudioBufferSizeIndex;
539     }
540 
541     m_syncBuffers = kDefaultSyncBuffers;
542     m_forceNetworkClock = false;
543 }
544 
getDevices() const545 QSet<SoundDeviceId> SoundManagerConfig::getDevices() const {
546     QSet<SoundDeviceId> devices;
547     devices.reserve(m_outputs.size() + m_inputs.size());
548     for (auto it = m_outputs.constBegin(); it != m_outputs.constEnd(); ++it) {
549         devices.insert(it.key());
550     }
551     for (auto it = m_inputs.constBegin(); it != m_inputs.constEnd(); ++it) {
552         devices.insert(it.key());
553     }
554     return devices;
555 }
556