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