From 1a99de151e494362c2f17cc1dad9415abafb1bf8 Mon Sep 17 00:00:00 2001 From: jshipman42 Date: Sun, 16 Aug 2026 03:17:16 -0400 Subject: [PATCH 1/6] sound: report the device name with each driver init error LoadAndInitializeFirstValidDriver() returned the error messages of the drivers it tried as a plain list, and the caller paired each message with a device name by indexing strDriverNames[] at the same position. That couples the error reporting to the layout of the driver list. Return the device name together with its error message instead, so the caller no longer has to know how the driver list is organized. No change in behaviour. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012i8guRg33BV5Prf7tqkkcH --- src/sound/soundbase.cpp | 18 +++++++++--------- src/sound/soundbase.h | 23 ++++++++++++++++++----- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/sound/soundbase.cpp b/src/sound/soundbase.cpp index 0581e6f80a..796f3e12f0 100644 --- a/src/sound/soundbase.cpp +++ b/src/sound/soundbase.cpp @@ -168,17 +168,17 @@ QString CSoundBase::SetDev ( const QString strDevName ) } // try to load and initialize any valid driver - QVector vsErrorList = LoadAndInitializeFirstValidDriver(); + QVector vErrorList = LoadAndInitializeFirstValidDriver(); - if ( !vsErrorList.isEmpty() ) + if ( !vErrorList.isEmpty() ) { // create error message with all details QString sErrorMessage = tr ( "%1 couldn't find a usable %2 audio device.

" ).arg ( APP_NAME ).arg ( strSystemDriverTechniqueName ); - for ( int i = 0; i < lNumDevs; i++ ) + for ( const CDriverInitError& DriverInitError : vErrorList ) { - sErrorMessage += "" + GetDeviceName ( i ) + ": " + vsErrorList[i] + "

"; + sErrorMessage += "" + DriverInitError.strDevName + ": " + DriverInitError.strError + "

"; } #if defined( _WIN32 ) && !defined( WITH_JACK ) @@ -202,9 +202,9 @@ QString CSoundBase::SetDev ( const QString strDevName ) return strReturn; } -QVector CSoundBase::LoadAndInitializeFirstValidDriver ( const bool bOpenDriverSetup ) +QVector CSoundBase::LoadAndInitializeFirstValidDriver ( const bool bOpenDriverSetup ) { - QVector vsErrorList; + QVector vErrorList; // load and initialize first valid ASIO driver bool bValidDriverDetected = false; @@ -216,7 +216,7 @@ QVector CSoundBase::LoadAndInitializeFirstValidDriver ( const bool bOpe // try to load and initialize current driver, store error message const QString strCurError = LoadAndInitializeDriver ( GetDeviceName ( iDriverCnt ), bOpenDriverSetup ); - vsErrorList.append ( strCurError ); + vErrorList.append ( CDriverInitError ( GetDeviceName ( iDriverCnt ), strCurError ) ); if ( strCurError.isEmpty() ) { @@ -227,14 +227,14 @@ QVector CSoundBase::LoadAndInitializeFirstValidDriver ( const bool bOpe strCurDevName = GetDeviceName ( iDriverCnt ); // empty error list shows that init was successful - vsErrorList.clear(); + vErrorList.clear(); } // try next driver iDriverCnt++; } - return vsErrorList; + return vErrorList; } /******************************************************************************\ diff --git a/src/sound/soundbase.h b/src/sound/soundbase.h index 32c727f999..8fc5dc4be2 100644 --- a/src/sound/soundbase.h +++ b/src/sound/soundbase.h @@ -84,6 +84,15 @@ class CMidiCtlEntry int iChannel; }; +// name of a device which could not be initialized together with the reason why +class CDriverInitError +{ +public: + CDriverInitError ( const QString& strNDevName = "", const QString& strNError = "" ) : strDevName ( strNDevName ), strError ( strNError ) {} + QString strDevName; + QString strError; +}; + /* Classes ********************************************************************/ class CSoundBase : public QThread { @@ -157,11 +166,15 @@ class CSoundBase : public QThread int iMuteMyselfCC ); protected: - virtual QString LoadAndInitializeDriver ( QString, bool ) { return ""; } - virtual void UnloadCurrentDriver() {} - QVector LoadAndInitializeFirstValidDriver ( const bool bOpenDriverSetup = false ); - void ParseCommandLineArgument ( const QString& strMIDISetup ); - QString GetDeviceName ( const int iDiD ) { return strDriverNames[iDiD]; } + virtual QString LoadAndInitializeDriver ( QString, bool ) { return ""; } + virtual void UnloadCurrentDriver() {} + + // returns an empty list if a driver could be initialized, otherwise the + // error message of each driver which was tried + virtual QVector LoadAndInitializeFirstValidDriver ( const bool bOpenDriverSetup = false ); + + void ParseCommandLineArgument ( const QString& strMIDISetup ); + QString GetDeviceName ( const int iDiD ) { return strDriverNames[iDiD]; } static void GetSelCHAndAddCH ( const int iSelCH, const int iNumInChan, int& iSelCHOut, int& iSelAddCHOut ) { From 42a739154e7316ddb8ab86a2fb3b1c6528ebe591 Mon Sep 17 00:00:00 2001 From: jshipman42 Date: Sun, 16 Aug 2026 03:17:24 -0400 Subject: [PATCH 2/6] sound, client: add optional separate input/output device selection Sound APIs differ in how they present devices: ASIO offers one driver which covers both directions, while CoreAudio manages the input and the output device independently of each other. Add an optional interface for the latter, which a sound API can enable by returning true from IsInOutDevSelectionSeparate() and filling one device list per direction. The selected device is still identified by one single (combined) device name, so GetDev()/SetDev() and the stored settings stay unchanged. No sound API implements the interface yet, so there is no change in behaviour. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012i8guRg33BV5Prf7tqkkcH --- src/client.cpp | 12 ++++++++++-- src/client.h | 12 +++++++++++- src/sound/soundbase.h | 13 +++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/client.cpp b/src/client.cpp index aa36cacb3f..ae5b20bee9 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -746,7 +746,14 @@ void CClient::SetAudioChannels ( const EAudChanConf eNAudChanConf ) } } -QString CClient::SetSndCrdDev ( const QString strNewDev ) +QString CClient::SetSndCrdDev ( const QString strNewDev ) { return ChangeSndCrdDev ( strNewDev, QString(), false ); } + +QString CClient::SetSndCrdInOutDev ( const QString& strNewInDev, const QString& strNewOutDev ) +{ + return ChangeSndCrdDev ( strNewInDev, strNewOutDev, true ); +} + +QString CClient::ChangeSndCrdDev ( const QString& strNewDev, const QString& strNewOutDev, const bool bSeparateInOutDev ) { QString strError = ""; @@ -762,7 +769,8 @@ QString CClient::SetSndCrdDev ( const QString strNewDev ) // on error condition. Catch it here as exceptions must not escape from a Qt slot. try { - strError = Sound.SetDev ( strNewDev ); + // for a separate selection the first parameter is the input device name + strError = bSeparateInOutDev ? Sound.SetInOutDev ( strNewDev, strNewOutDev ) : Sound.SetDev ( strNewDev ); // init again because the sound card actual buffer size might // be changed on new device diff --git a/src/client.h b/src/client.h index 1c4c4e5428..d5aff89095 100644 --- a/src/client.h +++ b/src/client.h @@ -244,6 +244,14 @@ class CClient : public QObject QString GetSndCrdDev() { return Sound.GetDev(); } void OpenSndCrdDriverSetup() { Sound.OpenDriverSetup(); } + // separate input/output device selection (only supported by some sound APIs) + bool GetSndCrdInOutDevSelectionSeparate() { return Sound.IsInOutDevSelectionSeparate(); } + QStringList GetSndCrdInputDevNames() { return Sound.GetInputDevNames(); } + QStringList GetSndCrdOutputDevNames() { return Sound.GetOutputDevNames(); } + QString GetSndCrdInputDev() { return Sound.GetInputDev(); } + QString GetSndCrdOutputDev() { return Sound.GetOutputDev(); } + QString SetSndCrdInOutDev ( const QString& strNewInDev, const QString& strNewOutDev ); + // sound card channel selection int GetSndCrdNumInputChannels() { return Sound.GetNumInputChannels(); } QString GetSndCrdInputChannelName ( const int iDiD ) { return Sound.GetInputChannelName ( iDiD ); } @@ -363,7 +371,9 @@ class CClient : public QObject void Start(); void Stop(); - void Init(); + void Init(); + QString ChangeSndCrdDev ( const QString& strNewDev, const QString& strNewOutDev, const bool bSeparateInOutDev ); + void ProcessSndCrdAudioData ( CVector& vecsStereoSndCrd ); void ProcessAudioDataIntern ( CVector& vecsStereoSndCrd ); diff --git a/src/sound/soundbase.h b/src/sound/soundbase.h index 8fc5dc4be2..424f048871 100644 --- a/src/sound/soundbase.h +++ b/src/sound/soundbase.h @@ -119,6 +119,19 @@ class CSoundBase : public QThread return strCurDevName; } + // Separate input/output device selection: sound APIs which handle the input + // and the output device independently of each other (i.e. CoreAudio on macOS) + // return true here and offer one device list per direction. For all other + // APIs a device is a single entity and the combined list above is used. + // Note that even with separate lists the selected device is still identified + // by one single (combined) device name, see GetDev()/SetDev(). + virtual bool IsInOutDevSelectionSeparate() const { return false; } + virtual QStringList GetInputDevNames() { return QStringList(); } + virtual QStringList GetOutputDevNames() { return QStringList(); } + virtual QString GetInputDev() { return QString(); } + virtual QString GetOutputDev() { return QString(); } + virtual QString SetInOutDev ( const QString& /* strInDevName */, const QString& /* strOutDevName */ ) { return QString(); } + virtual int GetNumInputChannels() { return 2; } virtual QString GetInputChannelName ( const int ) { return "Default"; } virtual void SetLeftInputChannel ( const int ) {} From 2360e99a0cd4450efdf967726ec33f4920149fc0 Mon Sep 17 00:00:00 2001 From: jshipman42 Date: Sun, 16 Aug 2026 03:18:16 -0400 Subject: [PATCH 3/6] macOS: enumerate the input and output devices separately CoreAudio manages the input and the output device independently, but the device list was built as the cartesian product of both so that a single combo box was sufficient in the GUI: // we add combined entries for input and output for each device so that we // do not need two combo boxes in the GUI for input and output (therefore // all possible combinations are required which can be a large number) The number of entries grows quadratically and the list is capped at MAX_NUMBER_SOUND_CARDS, so on a system with 12 input and 15 output devices the required 181 entries exceed the limit of 129 and the remaining combinations are silently dropped. Building the list also queries every device twice per combination, on each device change and dialog refresh. Enumerate the devices per direction instead and offer one combo box for the input and one for the output device. The selected device is still identified by one single combined name, so previously stored settings (including the "System Default In/Out Devices" entry) remain valid. CheckDeviceCapabilities() is split into an input and an output part, which also lets the search for the first usable device look for one device per direction instead of trying all combinations. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012i8guRg33BV5Prf7tqkkcH --- src/clientsettingsdlg.cpp | 98 ++++- src/clientsettingsdlg.h | 3 + src/clientsettingsdlgbase.ui | 54 +++ src/global.h | 7 +- src/sound/coreaudio-mac/sound.cpp | 583 ++++++++++++++++++++---------- src/sound/coreaudio-mac/sound.h | 39 +- 6 files changed, 567 insertions(+), 217 deletions(-) diff --git a/src/clientsettingsdlg.cpp b/src/clientsettingsdlg.cpp index ad92a0f8e1..148aef273a 100644 --- a/src/clientsettingsdlg.cpp +++ b/src/clientsettingsdlg.cpp @@ -142,18 +142,24 @@ CClientSettingsDlg::CClientSettingsDlg ( CClient* pNCliP, CClientSettings* pNSet #if !defined( WITH_JACK ) // sound card device - lblSoundcardDevice->setWhatsThis ( "" + tr ( "Audio Device" ) + ": " + - tr ( "Under the Windows operating system the ASIO driver (sound card) can be " - "selected using %1. If the selected ASIO driver is not valid an error " - "message is shown and the previous valid driver is selected. " - "Under macOS the input and output hardware can be selected." ) - .arg ( APP_NAME ) + - "
" + - tr ( "If the driver is selected during an active connection, the connection " - "is stopped, the driver is changed and the connection is started again " - "automatically." ) ); + const QString strAudioDevice = "" + tr ( "Audio Device" ) + ": " + + tr ( "Under the Windows operating system the ASIO driver (sound card) can be " + "selected using %1. If the selected ASIO driver is not valid an error " + "message is shown and the previous valid driver is selected. " + "Under macOS the input and output hardware can be selected." ) + .arg ( APP_NAME ) + + "
" + + tr ( "If the driver is selected during an active connection, the connection " + "is stopped, the driver is changed and the connection is started again " + "automatically." ); + + lblSoundcardDevice->setWhatsThis ( strAudioDevice ); + lblInputDevice->setWhatsThis ( strAudioDevice ); + lblOutputDevice->setWhatsThis ( strAudioDevice ); cbxSoundcard->setAccessibleName ( tr ( "Sound card device selector combo box" ) ); + cbxInputDevice->setAccessibleName ( tr ( "Audio input device selector combo box" ) ); + cbxOutputDevice->setAccessibleName ( tr ( "Audio output device selector combo box" ) ); # if defined( _WIN32 ) // set Windows specific tool tip @@ -735,6 +741,16 @@ CClientSettingsDlg::CClientSettingsDlg ( CClient* pNCliP, CClientSettings* pNSet this, &CClientSettingsDlg::OnSoundcardActivated ); + QObject::connect ( cbxInputDevice, + static_cast ( &QComboBox::activated ), + this, + &CClientSettingsDlg::OnInputDeviceActivated ); + + QObject::connect ( cbxOutputDevice, + static_cast ( &QComboBox::activated ), + this, + &CClientSettingsDlg::OnOutputDeviceActivated ); + QObject::connect ( cbxLInChan, static_cast ( &QComboBox::activated ), this, @@ -1126,18 +1142,44 @@ void CClientSettingsDlg::UpdateSoundCardFrame() } } -void CClientSettingsDlg::UpdateSoundDeviceChannelSelectionFrame() +void CClientSettingsDlg::UpdateSoundDeviceSelection() { - // update combo box containing all available sound cards in the system - QStringList slSndCrdDevNames = pClient->GetSndCrdDevNames(); - cbxSoundcard->clear(); - - foreach ( QString strDevName, slSndCrdDevNames ) + // Sound APIs which handle the input and the output device independently of + // each other (i.e. CoreAudio on macOS) get one combo box per direction. All + // other APIs get the single combo box containing the available devices. + const bool bSeparateInOutDev = pClient->GetSndCrdInOutDevSelectionSeparate(); + + lblSoundcardDevice->setVisible ( !bSeparateInOutDev ); + cbxSoundcard->setVisible ( !bSeparateInOutDev ); + lblInputDevice->setVisible ( bSeparateInOutDev ); + cbxInputDevice->setVisible ( bSeparateInOutDev ); + lblOutputDevice->setVisible ( bSeparateInOutDev ); + cbxOutputDevice->setVisible ( bSeparateInOutDev ); + + if ( bSeparateInOutDev ) + { + // update combo boxes containing the available input and output devices + cbxInputDevice->clear(); + cbxInputDevice->addItems ( pClient->GetSndCrdInputDevNames() ); + cbxInputDevice->setCurrentText ( pClient->GetSndCrdInputDev() ); + + cbxOutputDevice->clear(); + cbxOutputDevice->addItems ( pClient->GetSndCrdOutputDevNames() ); + cbxOutputDevice->setCurrentText ( pClient->GetSndCrdOutputDev() ); + } + else { - cbxSoundcard->addItem ( strDevName ); + // update combo box containing all available sound cards in the system + cbxSoundcard->clear(); + cbxSoundcard->addItems ( pClient->GetSndCrdDevNames() ); + cbxSoundcard->setCurrentText ( pClient->GetSndCrdDev() ); } +} - cbxSoundcard->setCurrentText ( pClient->GetSndCrdDev() ); +void CClientSettingsDlg::UpdateSoundDeviceChannelSelectionFrame() +{ + // update the sound device selection combo box(es) + UpdateSoundDeviceSelection(); // update input/output channel selection #if defined( _WIN32 ) || defined( __APPLE__ ) || defined( __MACOSX ) @@ -1222,6 +1264,26 @@ void CClientSettingsDlg::OnSoundcardActivated ( int iSndDevIdx ) UpdateDisplay(); } +void CClientSettingsDlg::OnInputDeviceActivated ( int iSndDevIdx ) +{ + // The device of the other direction is taken from its combo box and not from + // the sound API: if no device could be initialized at all, the sound API has + // no current device and we would compose an unusable device name from it. + pClient->SetSndCrdInOutDev ( cbxInputDevice->itemText ( iSndDevIdx ), cbxOutputDevice->currentText() ); + + UpdateSoundDeviceChannelSelectionFrame(); + UpdateDisplay(); +} + +void CClientSettingsDlg::OnOutputDeviceActivated ( int iSndDevIdx ) +{ + // the device of the other direction is taken from its combo box, see above + pClient->SetSndCrdInOutDev ( cbxInputDevice->currentText(), cbxOutputDevice->itemText ( iSndDevIdx ) ); + + UpdateSoundDeviceChannelSelectionFrame(); + UpdateDisplay(); +} + void CClientSettingsDlg::OnLInChanActivated ( int iChanIdx ) { pClient->SetSndCrdLeftInputChannel ( iChanIdx ); diff --git a/src/clientsettingsdlg.h b/src/clientsettingsdlg.h index 7844f64117..d78f378fc5 100644 --- a/src/clientsettingsdlg.h +++ b/src/clientsettingsdlg.h @@ -80,6 +80,7 @@ class CClientSettingsDlg : public CBaseDlg, private Ui_CClientSettingsDlgBase void UpdateUploadRate(); void UpdateDisplay(); + void UpdateSoundDeviceSelection(); void UpdateSoundDeviceChannelSelectionFrame(); void SetEnableFeedbackDetection ( bool enable ); @@ -111,6 +112,8 @@ public slots: void OnInputBoostChanged(); void OnSndCrdBufferDelayButtonGroupClicked ( QAbstractButton* button ); void OnSoundcardActivated ( int iSndDevIdx ); + void OnInputDeviceActivated ( int iSndDevIdx ); + void OnOutputDeviceActivated ( int iSndDevIdx ); void OnLInChanActivated ( int iChanIdx ); void OnRInChanActivated ( int iChanIdx ); void OnLOutChanActivated ( int iChanIdx ); diff --git a/src/clientsettingsdlgbase.ui b/src/clientsettingsdlgbase.ui index 842ab9220c..dcc849d769 100644 --- a/src/clientsettingsdlgbase.ui +++ b/src/clientsettingsdlgbase.ui @@ -396,6 +396,58 @@ + + + + Input Device + + + cbxInputDevice + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + + + + Output Device + + + cbxOutputDevice + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + @@ -2267,6 +2319,8 @@ spnMixerRows chbAudioAlerts cbxSoundcard + cbxInputDevice + cbxOutputDevice butDriverSetup cbxLInChan cbxRInChan diff --git a/src/global.h b/src/global.h index 5414a58c41..1e122862dc 100644 --- a/src/global.h +++ b/src/global.h @@ -205,8 +205,11 @@ LED bar: lbr // maximum number of fader groups (must be consistent to audiomixerboard implementation) #define MAX_NUM_FADER_GROUPS 8 -// maximum number of recognized sound cards installed in the system -#define MAX_NUMBER_SOUND_CARDS 129 // e.g. 16 inputs, 8 outputs + default entry (MacOS) +// maximum number of recognized sound cards installed in the system, i.e. the +// number of available ASIO drivers on Windows. On macOS the input and the +// output devices are enumerated separately, so the limit applies to each +// direction on its own (including the system default entry) +#define MAX_NUMBER_SOUND_CARDS 129 // define the maximum number of audio channel for input/output we can store // channel infos for (and therefore this is the maximum number of entries in diff --git a/src/sound/coreaudio-mac/sound.cpp b/src/sound/coreaudio-mac/sound.cpp index 589a77e1be..28832e82fd 100644 --- a/src/sound/coreaudio-mac/sound.cpp +++ b/src/sound/coreaudio-mac/sound.cpp @@ -46,6 +46,20 @@ #include "sound.h" +/* Definitions ****************************************************************/ +// Names of the entries which follow the device selected as system default. Note +// that these names are stored in the settings file and are therefore not +// translated. The combined name of the "both directions are system default" +// case is the one which was used when input and output were selected together +// so that settings written by previous versions are still valid. +static const QString strSystemDefaultInDevName = "System Default In Device"; +static const QString strSystemDefaultOutDevName = "System Default Out Device"; +static const QString strSystemDefaultInOutDevName = "System Default In/Out Devices"; + +// separators used for combining the input and output device name +static const QString strDevNameInPrefix = "in: "; +static const QString strDevNameOutPrefix = "/out: "; + /* Implementation *************************************************************/ CSound::CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), void* arg, const bool, const QString& ) : CSoundBase ( "CoreAudio", fpNewProcessCallback, arg ), @@ -68,8 +82,7 @@ CSound::CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* // initial query for available input/output sound devices in the system GetAvailableInOutDevices(); - // init device index as not initialized (invalid) - lCurDev = INVALID_INDEX; + // init device IDs as not initialized (invalid) CurrentAudioInputDeviceID = 0; CurrentAudioOutputDeviceID = 0; iNumInChan = 0; @@ -119,14 +132,16 @@ void CSound::GetAvailableInOutDevices() // calculate device count based on size of returned data array const UInt32 iDeviceCount = iPropertySize / sizeof ( AudioDeviceID ); - // always add system default devices for input and output as first entry - lNumDevs = 0; - strDriverNames[lNumDevs] = "System Default In/Out Devices"; + // always add the system default device as first entry of each list + lNumInDevs = 0; + lNumOutDevs = 0; + strInputDeviceNames[lNumInDevs] = strSystemDefaultInDevName; + strOutputDeviceNames[lNumOutDevs] = strSystemDefaultOutDevName; iPropertySize = sizeof ( AudioDeviceID ); stPropertyAddress.mSelector = kAudioHardwarePropertyDefaultInputDevice; - if ( AudioObjectGetPropertyData ( kAudioObjectSystemObject, &stPropertyAddress, 0, NULL, &iPropertySize, &audioInputDevice[lNumDevs] ) ) + if ( AudioObjectGetPropertyData ( kAudioObjectSystemObject, &stPropertyAddress, 0, NULL, &iPropertySize, &audioInputDevice[lNumInDevs] ) ) { throw CGenErr ( tr ( "No sound card is available in your system. " "CoreAudio input AudioHardwareGetProperty call failed." ) ); @@ -135,49 +150,149 @@ void CSound::GetAvailableInOutDevices() iPropertySize = sizeof ( AudioDeviceID ); stPropertyAddress.mSelector = kAudioHardwarePropertyDefaultOutputDevice; - if ( AudioObjectGetPropertyData ( kAudioObjectSystemObject, &stPropertyAddress, 0, NULL, &iPropertySize, &audioOutputDevice[lNumDevs] ) ) + if ( AudioObjectGetPropertyData ( kAudioObjectSystemObject, &stPropertyAddress, 0, NULL, &iPropertySize, &audioOutputDevice[lNumOutDevs] ) ) { throw CGenErr ( tr ( "No sound card is available in the system. " "CoreAudio output AudioHardwareGetProperty call failed." ) ); } - lNumDevs++; // next device + lNumInDevs++; // next input device + lNumOutDevs++; // next output device - // add detected devices - // - // we add combined entries for input and output for each device so that we - // do not need two combo boxes in the GUI for input and output (therefore - // all possible combinations are required which can be a large number) + // add the detected devices to the input and/or the output list (a device + // which offers both directions shows up in both lists) for ( UInt32 i = 0; i < iDeviceCount; i++ ) { - for ( UInt32 j = 0; j < iDeviceCount; j++ ) + QString strDeviceName; + bool bIsInput; + bool bIsOutput; + + GetAudioDeviceInfos ( vAudioDevices[i], strDeviceName, bIsInput, bIsOutput ); + + if ( bIsInput && ( lNumInDevs < MAX_NUMBER_SOUND_CARDS ) ) { - // get device infos for both current devices - QString strDeviceName_i; - QString strDeviceName_j; - bool bIsInput_i; - bool bIsInput_j; - bool bIsOutput_i; - bool bIsOutput_j; + strInputDeviceNames[lNumInDevs] = strDeviceName; + audioInputDevice[lNumInDevs] = vAudioDevices[i]; - GetAudioDeviceInfos ( vAudioDevices[i], strDeviceName_i, bIsInput_i, bIsOutput_i ); + lNumInDevs++; // next input device + } - GetAudioDeviceInfos ( vAudioDevices[j], strDeviceName_j, bIsInput_j, bIsOutput_j ); + if ( bIsOutput && ( lNumOutDevs < MAX_NUMBER_SOUND_CARDS ) ) + { + strOutputDeviceNames[lNumOutDevs] = strDeviceName; + audioOutputDevice[lNumOutDevs] = vAudioDevices[i]; - // check if i device is input and j device is output and that we are - // in range - if ( bIsInput_i && bIsOutput_j && ( lNumDevs < MAX_NUMBER_SOUND_CARDS ) ) - { - strDriverNames[lNumDevs] = "in: " + strDeviceName_i + "/out: " + strDeviceName_j; + lNumOutDevs++; // next output device + } + } +} - // store audio device IDs - audioInputDevice[lNumDevs] = vAudioDevices[i]; - audioOutputDevice[lNumDevs] = vAudioDevices[j]; +QStringList CSound::GetInputDevNames() +{ + QMutexLocker locker ( &Mutex ); - lNumDevs++; // next device - } + // note that the device list is refreshed whenever a driver is loaded + QStringList slDevNames; + + for ( int iDev = 0; iDev < lNumInDevs; iDev++ ) + { + slDevNames << strInputDeviceNames[iDev]; + } + + return slDevNames; +} + +QStringList CSound::GetOutputDevNames() +{ + QMutexLocker locker ( &Mutex ); + + // note that the device list is refreshed whenever a driver is loaded + QStringList slDevNames; + + for ( int iDev = 0; iDev < lNumOutDevs; iDev++ ) + { + slDevNames << strOutputDeviceNames[iDev]; + } + + return slDevNames; +} + +QString CSound::GetInputDev() +{ + QMutexLocker locker ( &Mutex ); + + return strCurInDevName; +} + +QString CSound::GetOutputDev() +{ + QMutexLocker locker ( &Mutex ); + + return strCurOutDevName; +} + +QString CSound::SetInOutDev ( const QString& strInDevName, const QString& strOutDevName ) +{ + // the base class handles the device change based on the combined name + return SetDev ( ComposeDevName ( strInDevName, strOutDevName ) ); +} + +QString CSound::ComposeDevName ( const QString& strInDevName, const QString& strOutDevName ) +{ + // if both directions use the system default device, use the legacy name of + // the combined entry so that the settings stay compatible + if ( ( strInDevName.compare ( strSystemDefaultInDevName ) == 0 ) && ( strOutDevName.compare ( strSystemDefaultOutDevName ) == 0 ) ) + { + return strSystemDefaultInOutDevName; + } + + return strDevNameInPrefix + strInDevName + strDevNameOutPrefix + strOutDevName; +} + +int CSound::FindDevIdx ( const QString strDevNames[], const long lNumDevices, const QString& strDevName ) const +{ + for ( int i = 0; i < lNumDevices; i++ ) + { + if ( strDevNames[i].compare ( strDevName ) == 0 ) + { + return i; } } + + return INVALID_INDEX; +} + +bool CSound::SplitAndResolveDevName ( const QString& strDevName, int& iInDevIdx, int& iOutDevIdx ) const +{ + iInDevIdx = INVALID_INDEX; + iOutDevIdx = INVALID_INDEX; + + if ( strDevName.compare ( strSystemDefaultInOutDevName ) == 0 ) + { + // legacy name of the combined system default entry + iInDevIdx = FindDevIdx ( strInputDeviceNames, lNumInDevs, strSystemDefaultInDevName ); + iOutDevIdx = FindDevIdx ( strOutputDeviceNames, lNumOutDevs, strSystemDefaultOutDevName ); + } + else if ( strDevName.startsWith ( strDevNameInPrefix ) ) + { + const int iInPrefixLen = static_cast ( strDevNameInPrefix.length() ); + const int iOutPrefixLen = static_cast ( strDevNameOutPrefix.length() ); + + // a device name may itself contain the separator, therefore try all + // occurrences until both parts resolve to an available device + int iSepPos = static_cast ( strDevName.indexOf ( strDevNameOutPrefix, iInPrefixLen ) ); + + while ( ( iSepPos >= 0 ) && ( ( iInDevIdx == INVALID_INDEX ) || ( iOutDevIdx == INVALID_INDEX ) ) ) + { + iInDevIdx = FindDevIdx ( strInputDeviceNames, lNumInDevs, strDevName.mid ( iInPrefixLen, iSepPos - iInPrefixLen ) ); + + iOutDevIdx = FindDevIdx ( strOutputDeviceNames, lNumOutDevs, strDevName.mid ( iSepPos + iOutPrefixLen ) ); + + iSepPos = static_cast ( strDevName.indexOf ( strDevNameOutPrefix, iSepPos + 1 ) ); + } + } + + return ( iInDevIdx != INVALID_INDEX ) && ( iOutDevIdx != INVALID_INDEX ); } void CSound::GetAudioDeviceInfos ( const AudioDeviceID DeviceID, QString& strDeviceName, bool& bIsInput, bool& bIsOutput ) @@ -279,118 +394,179 @@ int CSound::CountChannels ( AudioDeviceID devID, bool isInput ) QString CSound::LoadAndInitializeDriver ( QString strDriverName, bool ) { - // secure lNumDevs/strDriverNames access + // secure device list access QMutexLocker locker ( &Mutex ); - // reload the driver list of available sound devices + // reload the list of available sound devices GetAvailableInOutDevices(); - // find driver index from given driver name - int iDriverIdx = INVALID_INDEX; // initialize with an invalid index + // find the input and output device index from the given combined name + int iInDevIdx; + int iOutDevIdx; - for ( int i = 0; i < MAX_NUMBER_SOUND_CARDS; i++ ) + // if one of the selected devices was not found, return an error message + if ( !SplitAndResolveDevName ( strDriverName, iInDevIdx, iOutDevIdx ) ) { - if ( strDriverName.compare ( strDriverNames[i] ) == 0 ) - { - iDriverIdx = i; - } + return tr ( "The currently selected audio device is no longer present. Please check your audio device." ); } - // if the selected driver was not found, return an error message - if ( iDriverIdx == INVALID_INDEX ) + // check device capabilities if they fulfill our requirements + QString strStat = CheckInputDeviceCapabilities ( iInDevIdx ); + + if ( strStat.isEmpty() ) { - return tr ( "The currently selected audio device is no longer present. Please check your audio device." ); + strStat = CheckOutputDeviceCapabilities ( iOutDevIdx ); } - // check device capabilities if it fulfills our requirements - const QString strStat = CheckDeviceCapabilities ( iDriverIdx ); + if ( strStat.isEmpty() ) + { + ApplyDeviceSelection ( iInDevIdx, iOutDevIdx ); + } + + return strStat; +} + +QVector CSound::LoadAndInitializeFirstValidDriver ( const bool ) +{ + // secure device list access + QMutexLocker locker ( &Mutex ); + + // reload the list of available sound devices + GetAvailableInOutDevices(); - // check if device is capable and if not the same device is used - if ( strStat.isEmpty() && ( strCurDevName.compare ( strDriverNames[iDriverIdx] ) != 0 ) ) + // since the input and the output device are independent of each other, we + // can search for a usable device per direction instead of trying all + // possible combinations + QVector vErrorList; + + int iValidInDevIdx = INVALID_INDEX; + int iValidOutDevIdx = INVALID_INDEX; + + for ( int iInDevIdx = 0; ( iInDevIdx < lNumInDevs ) && ( iValidInDevIdx == INVALID_INDEX ); iInDevIdx++ ) { - AudioObjectPropertyAddress stPropertyAddress; + const QString strCurError = CheckInputDeviceCapabilities ( iInDevIdx ); - // unregister callbacks if previous device was valid - if ( lCurDev != INVALID_INDEX ) + if ( strCurError.isEmpty() ) + { + iValidInDevIdx = iInDevIdx; + } + else { - stPropertyAddress.mElement = kAudioObjectPropertyElementMaster; - stPropertyAddress.mScope = kAudioObjectPropertyScopeGlobal; + vErrorList.append ( CDriverInitError ( strInputDeviceNames[iInDevIdx], strCurError ) ); + } + } - // unregister callback functions for device property changes - stPropertyAddress.mSelector = kAudioHardwarePropertyDefaultOutputDevice; + for ( int iOutDevIdx = 0; ( iOutDevIdx < lNumOutDevs ) && ( iValidOutDevIdx == INVALID_INDEX ); iOutDevIdx++ ) + { + const QString strCurError = CheckOutputDeviceCapabilities ( iOutDevIdx ); - AudioObjectRemovePropertyListener ( kAudioObjectSystemObject, &stPropertyAddress, deviceNotification, this ); + if ( strCurError.isEmpty() ) + { + iValidOutDevIdx = iOutDevIdx; + } + else + { + vErrorList.append ( CDriverInitError ( strOutputDeviceNames[iOutDevIdx], strCurError ) ); + } + } - stPropertyAddress.mSelector = kAudioHardwarePropertyDefaultInputDevice; + if ( ( iValidInDevIdx != INVALID_INDEX ) && ( iValidOutDevIdx != INVALID_INDEX ) ) + { + ApplyDeviceSelection ( iValidInDevIdx, iValidOutDevIdx ); - AudioObjectRemovePropertyListener ( kAudioObjectSystemObject, &stPropertyAddress, deviceNotification, this ); + // empty error list shows that init was successful + vErrorList.clear(); + } - stPropertyAddress.mSelector = kAudioDevicePropertyDeviceHasChanged; + return vErrorList; +} - AudioObjectRemovePropertyListener ( audioOutputDevice[lCurDev], &stPropertyAddress, deviceNotification, this ); +void CSound::ApplyDeviceSelection ( const int iInDevIdx, const int iOutDevIdx ) +{ + const QString strNewDevName = ComposeDevName ( strInputDeviceNames[iInDevIdx], strOutputDeviceNames[iOutDevIdx] ); - AudioObjectRemovePropertyListener ( audioInputDevice[lCurDev], &stPropertyAddress, deviceNotification, this ); + // nothing to do if the selected devices are already in use + if ( strCurDevName.compare ( strNewDevName ) == 0 ) + { + return; + } - stPropertyAddress.mSelector = kAudioDevicePropertyDeviceIsAlive; + AudioObjectPropertyAddress stPropertyAddress; - AudioObjectRemovePropertyListener ( audioOutputDevice[lCurDev], &stPropertyAddress, deviceNotification, this ); + stPropertyAddress.mElement = kAudioObjectPropertyElementMaster; + stPropertyAddress.mScope = kAudioObjectPropertyScopeGlobal; - AudioObjectRemovePropertyListener ( audioInputDevice[lCurDev], &stPropertyAddress, deviceNotification, this ); - } + // unregister the callback functions if devices were already selected (a + // device ID of zero means that no device was registered so far) + if ( CurrentAudioInputDeviceID != 0 ) + { + stPropertyAddress.mSelector = kAudioHardwarePropertyDefaultOutputDevice; - // store ID of selected driver if initialization was successful - lCurDev = iDriverIdx; - CurrentAudioInputDeviceID = audioInputDevice[iDriverIdx]; - CurrentAudioOutputDeviceID = audioOutputDevice[iDriverIdx]; + AudioObjectRemovePropertyListener ( kAudioObjectSystemObject, &stPropertyAddress, deviceNotification, this ); - // register callbacks - stPropertyAddress.mElement = kAudioObjectPropertyElementMaster; - stPropertyAddress.mScope = kAudioObjectPropertyScopeGlobal; + stPropertyAddress.mSelector = kAudioHardwarePropertyDefaultInputDevice; + + AudioObjectRemovePropertyListener ( kAudioObjectSystemObject, &stPropertyAddress, deviceNotification, this ); - // setup callbacks for device property changes stPropertyAddress.mSelector = kAudioDevicePropertyDeviceHasChanged; - AudioObjectAddPropertyListener ( audioInputDevice[lCurDev], &stPropertyAddress, deviceNotification, this ); + AudioObjectRemovePropertyListener ( CurrentAudioOutputDeviceID, &stPropertyAddress, deviceNotification, this ); - AudioObjectAddPropertyListener ( audioOutputDevice[lCurDev], &stPropertyAddress, deviceNotification, this ); + AudioObjectRemovePropertyListener ( CurrentAudioInputDeviceID, &stPropertyAddress, deviceNotification, this ); stPropertyAddress.mSelector = kAudioDevicePropertyDeviceIsAlive; - AudioObjectAddPropertyListener ( audioInputDevice[lCurDev], &stPropertyAddress, deviceNotification, this ); + AudioObjectRemovePropertyListener ( CurrentAudioOutputDeviceID, &stPropertyAddress, deviceNotification, this ); - AudioObjectAddPropertyListener ( audioOutputDevice[lCurDev], &stPropertyAddress, deviceNotification, this ); + AudioObjectRemovePropertyListener ( CurrentAudioInputDeviceID, &stPropertyAddress, deviceNotification, this ); + } - stPropertyAddress.mSelector = kAudioHardwarePropertyDefaultOutputDevice; + // store the selected devices + CurrentAudioInputDeviceID = audioInputDevice[iInDevIdx]; + CurrentAudioOutputDeviceID = audioOutputDevice[iOutDevIdx]; + strCurInDevName = strInputDeviceNames[iInDevIdx]; + strCurOutDevName = strOutputDeviceNames[iOutDevIdx]; + strCurDevName = strNewDevName; - AudioObjectAddPropertyListener ( kAudioObjectSystemObject, &stPropertyAddress, deviceNotification, this ); + // setup callbacks for device property changes + stPropertyAddress.mSelector = kAudioDevicePropertyDeviceHasChanged; - stPropertyAddress.mSelector = kAudioHardwarePropertyDefaultInputDevice; + AudioObjectAddPropertyListener ( CurrentAudioInputDeviceID, &stPropertyAddress, deviceNotification, this ); - AudioObjectAddPropertyListener ( kAudioObjectSystemObject, &stPropertyAddress, deviceNotification, this ); + AudioObjectAddPropertyListener ( CurrentAudioOutputDeviceID, &stPropertyAddress, deviceNotification, this ); - // the device has changed, per definition we reset the channel - // mapping to the defaults (first two available channels) - SetLeftInputChannel ( 0 ); - SetRightInputChannel ( 1 ); - SetLeftOutputChannel ( 0 ); - SetRightOutputChannel ( 1 ); + stPropertyAddress.mSelector = kAudioDevicePropertyDeviceIsAlive; - // store the current name of the driver - strCurDevName = strDriverNames[iDriverIdx]; - } + AudioObjectAddPropertyListener ( CurrentAudioInputDeviceID, &stPropertyAddress, deviceNotification, this ); - return strStat; + AudioObjectAddPropertyListener ( CurrentAudioOutputDeviceID, &stPropertyAddress, deviceNotification, this ); + + stPropertyAddress.mSelector = kAudioHardwarePropertyDefaultOutputDevice; + + AudioObjectAddPropertyListener ( kAudioObjectSystemObject, &stPropertyAddress, deviceNotification, this ); + + stPropertyAddress.mSelector = kAudioHardwarePropertyDefaultInputDevice; + + AudioObjectAddPropertyListener ( kAudioObjectSystemObject, &stPropertyAddress, deviceNotification, this ); + + // the device has changed, per definition we reset the channel + // mapping to the defaults (first two available channels) + SetLeftInputChannel ( 0 ); + SetRightInputChannel ( 1 ); + SetLeftOutputChannel ( 0 ); + SetRightOutputChannel ( 1 ); } -QString CSound::CheckDeviceCapabilities ( const int iDriverIdx ) +QString CSound::CheckInputDeviceCapabilities ( const int iInDevIdx ) { UInt32 iPropertySize; AudioStreamBasicDescription CurDevStreamFormat; Float64 inputSampleRate = 0; - Float64 outputSampleRate = 0; const Float64 fSystemSampleRate = static_cast ( SYSTEM_SAMPLE_RATE_HZ ); AudioObjectPropertyAddress stPropertyAddress; + const AudioDeviceID InputDeviceID = audioInputDevice[iInDevIdx]; + stPropertyAddress.mScope = kAudioObjectPropertyScopeGlobal; stPropertyAddress.mElement = kAudioObjectPropertyElementMaster; @@ -398,7 +574,7 @@ QString CSound::CheckDeviceCapabilities ( const int iDriverIdx ) stPropertyAddress.mSelector = kAudioDevicePropertyNominalSampleRate; iPropertySize = sizeof ( Float64 ); - if ( AudioObjectGetPropertyData ( audioInputDevice[iDriverIdx], &stPropertyAddress, 0, NULL, &iPropertySize, &inputSampleRate ) ) + if ( AudioObjectGetPropertyData ( InputDeviceID, &stPropertyAddress, 0, NULL, &iPropertySize, &inputSampleRate ) ) { return QString ( tr ( "The audio input device is no longer available. Please check if your input device is connected correctly." ) ); } @@ -406,8 +582,7 @@ QString CSound::CheckDeviceCapabilities ( const int iDriverIdx ) if ( inputSampleRate != fSystemSampleRate ) { // try to change the sample rate - if ( AudioObjectSetPropertyData ( audioInputDevice[iDriverIdx], &stPropertyAddress, 0, NULL, sizeof ( Float64 ), &fSystemSampleRate ) != - noErr ) + if ( AudioObjectSetPropertyData ( InputDeviceID, &stPropertyAddress, 0, NULL, sizeof ( Float64 ), &fSystemSampleRate ) != noErr ) { return QString ( tr ( "The sample rate on the current input device isn't %1 Hz and is therefore incompatible. " "Please select another device or try setting the sample rate to %1 Hz " @@ -416,58 +591,23 @@ QString CSound::CheckDeviceCapabilities ( const int iDriverIdx ) } } - // check output device sample rate - iPropertySize = sizeof ( Float64 ); - - if ( AudioObjectGetPropertyData ( audioOutputDevice[iDriverIdx], &stPropertyAddress, 0, NULL, &iPropertySize, &outputSampleRate ) ) - { - return QString ( tr ( "The audio output device is no longer available. Please check if your output device is connected correctly." ) ); - } - - if ( outputSampleRate != fSystemSampleRate ) - { - // try to change the sample rate - if ( AudioObjectSetPropertyData ( audioOutputDevice[iDriverIdx], &stPropertyAddress, 0, NULL, sizeof ( Float64 ), &fSystemSampleRate ) != - noErr ) - { - return QString ( tr ( "The sample rate on the current output device isn't %1 Hz and is therefore incompatible. " - "Please select another device or try setting the sample rate to %1 Hz " - "manually via Audio-MIDI-Setup (in Applications->Utilities)." ) ) - .arg ( SYSTEM_SAMPLE_RATE_HZ ); - } - } - // get the stream ID of the input device (at least one stream must always exist) iPropertySize = 0; stPropertyAddress.mSelector = kAudioDevicePropertyStreams; stPropertyAddress.mScope = kAudioObjectPropertyScopeInput; - AudioObjectGetPropertyDataSize ( audioInputDevice[iDriverIdx], &stPropertyAddress, 0, NULL, &iPropertySize ); + AudioObjectGetPropertyDataSize ( InputDeviceID, &stPropertyAddress, 0, NULL, &iPropertySize ); CVector vInputStreamIDList ( iPropertySize ); - AudioObjectGetPropertyData ( audioInputDevice[iDriverIdx], &stPropertyAddress, 0, NULL, &iPropertySize, &vInputStreamIDList[0] ); + AudioObjectGetPropertyData ( InputDeviceID, &stPropertyAddress, 0, NULL, &iPropertySize, &vInputStreamIDList[0] ); const AudioStreamID inputStreamID = vInputStreamIDList[0]; - // get the stream ID of the output device (at least one stream must always exist) - iPropertySize = 0; - stPropertyAddress.mSelector = kAudioDevicePropertyStreams; - stPropertyAddress.mScope = kAudioObjectPropertyScopeOutput; - - AudioObjectGetPropertyDataSize ( audioOutputDevice[iDriverIdx], &stPropertyAddress, 0, NULL, &iPropertySize ); - - CVector vOutputStreamIDList ( iPropertySize ); - - AudioObjectGetPropertyData ( audioOutputDevice[iDriverIdx], &stPropertyAddress, 0, NULL, &iPropertySize, &vOutputStreamIDList[0] ); - - const AudioStreamID outputStreamID = vOutputStreamIDList[0]; - // According to the AudioHardware documentation: "If the format is a linear PCM // format, the data will always be presented as 32 bit, native endian floating // point. All conversions to and from the true physical format of the hardware // is handled by the devices driver.". - // check the input iPropertySize = sizeof ( AudioStreamBasicDescription ); stPropertyAddress.mSelector = kAudioStreamPropertyVirtualFormat; stPropertyAddress.mScope = kAudioObjectPropertyScopeGlobal; @@ -482,31 +622,14 @@ QString CSound::CheckDeviceCapabilities ( const int iDriverIdx ) "compatible with this software. Please select another device." ) ); } - // check the output - AudioObjectGetPropertyData ( outputStreamID, &stPropertyAddress, 0, NULL, &iPropertySize, &CurDevStreamFormat ); + // store the number of input channels for this device + iNumInChan = CountChannels ( InputDeviceID, true ); - if ( ( CurDevStreamFormat.mFormatID != kAudioFormatLinearPCM ) || ( CurDevStreamFormat.mFramesPerPacket != 1 ) || - ( CurDevStreamFormat.mBitsPerChannel != 32 ) || ( !( CurDevStreamFormat.mFormatFlags & kAudioFormatFlagIsFloat ) ) || - ( !( CurDevStreamFormat.mFormatFlags & kAudioFormatFlagIsPacked ) ) ) - { - return QString ( tr ( "The stream format on the current output device isn't " - "compatible with %1. Please select another device." ) ) - .arg ( APP_NAME ); - } - - // store the input and out number of channels for this device - iNumInChan = CountChannels ( audioInputDevice[iDriverIdx], true ); - iNumOutChan = CountChannels ( audioOutputDevice[iDriverIdx], false ); - - // clip the number of input/output channels to our allowed maximum + // clip the number of input channels to our allowed maximum if ( iNumInChan > MAX_NUM_IN_OUT_CHANNELS ) { iNumInChan = MAX_NUM_IN_OUT_CHANNELS; } - if ( iNumOutChan > MAX_NUM_IN_OUT_CHANNELS ) - { - iNumOutChan = MAX_NUM_IN_OUT_CHANNELS; - } // get the channel names of the input device for ( int iCurInCH = 0; iCurInCH < iNumInChan; iCurInCH++ ) @@ -518,7 +641,7 @@ QString CSound::CheckDeviceCapabilities ( const int iDriverIdx ) stPropertyAddress.mScope = kAudioObjectPropertyScopeInput; iPropertySize = sizeof ( CFStringRef ); - AudioObjectGetPropertyData ( audioInputDevice[iDriverIdx], &stPropertyAddress, 0, NULL, &iPropertySize, &sPropertyStringValue ); + AudioObjectGetPropertyData ( InputDeviceID, &stPropertyAddress, 0, NULL, &iPropertySize, &sPropertyStringValue ); // convert string const bool bConvOK = ConvertCFStringToQString ( sPropertyStringValue, sChannelNamesInput[iCurInCH] ); @@ -535,6 +658,107 @@ QString CSound::CheckDeviceCapabilities ( const int iDriverIdx ) } } + // special case with 4 input channels: support adding channels + if ( iNumInChan == 4 ) + { + // add four mixed channels (i.e. 4 normal, 4 mixed channels) + iNumInChanPlusAddChan = 8; + + for ( int iCh = 0; iCh < iNumInChanPlusAddChan; iCh++ ) + { + int iSelCH, iSelAddCH; + + GetSelCHAndAddCH ( iCh, iNumInChan, iSelCH, iSelAddCH ); + + if ( iSelAddCH >= 0 ) + { + // for mixed channels, show both audio channel names to be mixed + sChannelNamesInput[iCh] = sChannelNamesInput[iSelCH] + " + " + sChannelNamesInput[iSelAddCH]; + } + } + } + else + { + // regular case: no mixing input channels used + iNumInChanPlusAddChan = iNumInChan; + } + + // everything is ok, return empty string for "no error" case + return ""; +} + +QString CSound::CheckOutputDeviceCapabilities ( const int iOutDevIdx ) +{ + UInt32 iPropertySize; + AudioStreamBasicDescription CurDevStreamFormat; + Float64 outputSampleRate = 0; + const Float64 fSystemSampleRate = static_cast ( SYSTEM_SAMPLE_RATE_HZ ); + AudioObjectPropertyAddress stPropertyAddress; + + const AudioDeviceID OutputDeviceID = audioOutputDevice[iOutDevIdx]; + + stPropertyAddress.mScope = kAudioObjectPropertyScopeGlobal; + stPropertyAddress.mElement = kAudioObjectPropertyElementMaster; + + // check output device sample rate + stPropertyAddress.mSelector = kAudioDevicePropertyNominalSampleRate; + iPropertySize = sizeof ( Float64 ); + + if ( AudioObjectGetPropertyData ( OutputDeviceID, &stPropertyAddress, 0, NULL, &iPropertySize, &outputSampleRate ) ) + { + return QString ( tr ( "The audio output device is no longer available. Please check if your output device is connected correctly." ) ); + } + + if ( outputSampleRate != fSystemSampleRate ) + { + // try to change the sample rate + if ( AudioObjectSetPropertyData ( OutputDeviceID, &stPropertyAddress, 0, NULL, sizeof ( Float64 ), &fSystemSampleRate ) != noErr ) + { + return QString ( tr ( "The sample rate on the current output device isn't %1 Hz and is therefore incompatible. " + "Please select another device or try setting the sample rate to %1 Hz " + "manually via Audio-MIDI-Setup (in Applications->Utilities)." ) ) + .arg ( SYSTEM_SAMPLE_RATE_HZ ); + } + } + + // get the stream ID of the output device (at least one stream must always exist) + iPropertySize = 0; + stPropertyAddress.mSelector = kAudioDevicePropertyStreams; + stPropertyAddress.mScope = kAudioObjectPropertyScopeOutput; + + AudioObjectGetPropertyDataSize ( OutputDeviceID, &stPropertyAddress, 0, NULL, &iPropertySize ); + + CVector vOutputStreamIDList ( iPropertySize ); + + AudioObjectGetPropertyData ( OutputDeviceID, &stPropertyAddress, 0, NULL, &iPropertySize, &vOutputStreamIDList[0] ); + + const AudioStreamID outputStreamID = vOutputStreamIDList[0]; + + // check the stream format (see the comment in CheckInputDeviceCapabilities()) + iPropertySize = sizeof ( AudioStreamBasicDescription ); + stPropertyAddress.mSelector = kAudioStreamPropertyVirtualFormat; + stPropertyAddress.mScope = kAudioObjectPropertyScopeGlobal; + + AudioObjectGetPropertyData ( outputStreamID, &stPropertyAddress, 0, NULL, &iPropertySize, &CurDevStreamFormat ); + + if ( ( CurDevStreamFormat.mFormatID != kAudioFormatLinearPCM ) || ( CurDevStreamFormat.mFramesPerPacket != 1 ) || + ( CurDevStreamFormat.mBitsPerChannel != 32 ) || ( !( CurDevStreamFormat.mFormatFlags & kAudioFormatFlagIsFloat ) ) || + ( !( CurDevStreamFormat.mFormatFlags & kAudioFormatFlagIsPacked ) ) ) + { + return QString ( tr ( "The stream format on the current output device isn't " + "compatible with %1. Please select another device." ) ) + .arg ( APP_NAME ); + } + + // store the number of output channels for this device + iNumOutChan = CountChannels ( OutputDeviceID, false ); + + // clip the number of output channels to our allowed maximum + if ( iNumOutChan > MAX_NUM_IN_OUT_CHANNELS ) + { + iNumOutChan = MAX_NUM_IN_OUT_CHANNELS; + } + // get the channel names of the output device for ( int iCurOutCH = 0; iCurOutCH < iNumOutChan; iCurOutCH++ ) { @@ -545,7 +769,7 @@ QString CSound::CheckDeviceCapabilities ( const int iDriverIdx ) stPropertyAddress.mScope = kAudioObjectPropertyScopeOutput; iPropertySize = sizeof ( CFStringRef ); - AudioObjectGetPropertyData ( audioOutputDevice[iDriverIdx], &stPropertyAddress, 0, NULL, &iPropertySize, &sPropertyStringValue ); + AudioObjectGetPropertyData ( OutputDeviceID, &stPropertyAddress, 0, NULL, &iPropertySize, &sPropertyStringValue ); // convert string const bool bConvOK = ConvertCFStringToQString ( sPropertyStringValue, sChannelNamesOutput[iCurOutCH] ); @@ -562,31 +786,6 @@ QString CSound::CheckDeviceCapabilities ( const int iDriverIdx ) } } - // special case with 4 input channels: support adding channels - if ( iNumInChan == 4 ) - { - // add four mixed channels (i.e. 4 normal, 4 mixed channels) - iNumInChanPlusAddChan = 8; - - for ( int iCh = 0; iCh < iNumInChanPlusAddChan; iCh++ ) - { - int iSelCH, iSelAddCH; - - GetSelCHAndAddCH ( iCh, iNumInChan, iSelCH, iSelAddCH ); - - if ( iSelAddCH >= 0 ) - { - // for mixed channels, show both audio channel names to be mixed - sChannelNamesInput[iCh] = sChannelNamesInput[iSelCH] + " + " + sChannelNamesInput[iSelAddCH]; - } - } - } - else - { - // regular case: no mixing input channels used - iNumInChanPlusAddChan = iNumInChan; - } - // everything is ok, return empty string for "no error" case return ""; } @@ -710,13 +909,13 @@ void CSound::SetRightOutputChannel ( const int iNewChan ) void CSound::Start() { // register the callback function for input and output - AudioDeviceCreateIOProcID ( audioInputDevice[lCurDev], callbackIO, this, &audioInputProcID ); + AudioDeviceCreateIOProcID ( CurrentAudioInputDeviceID, callbackIO, this, &audioInputProcID ); - AudioDeviceCreateIOProcID ( audioOutputDevice[lCurDev], callbackIO, this, &audioOutputProcID ); + AudioDeviceCreateIOProcID ( CurrentAudioOutputDeviceID, callbackIO, this, &audioOutputProcID ); // start the audio stream - AudioDeviceStart ( audioInputDevice[lCurDev], audioInputProcID ); - AudioDeviceStart ( audioOutputDevice[lCurDev], audioOutputProcID ); + AudioDeviceStart ( CurrentAudioInputDeviceID, audioInputProcID ); + AudioDeviceStart ( CurrentAudioOutputDeviceID, audioOutputProcID ); // call base class CSoundBase::Start(); @@ -725,12 +924,12 @@ void CSound::Start() void CSound::Stop() { // stop the audio stream - AudioDeviceStop ( audioInputDevice[lCurDev], audioInputProcID ); - AudioDeviceStop ( audioOutputDevice[lCurDev], audioOutputProcID ); + AudioDeviceStop ( CurrentAudioInputDeviceID, audioInputProcID ); + AudioDeviceStop ( CurrentAudioOutputDeviceID, audioOutputProcID ); // unregister the callback function for input and output - AudioDeviceDestroyIOProcID ( audioInputDevice[lCurDev], audioInputProcID ); - AudioDeviceDestroyIOProcID ( audioOutputDevice[lCurDev], audioOutputProcID ); + AudioDeviceDestroyIOProcID ( CurrentAudioInputDeviceID, audioInputProcID ); + AudioDeviceDestroyIOProcID ( CurrentAudioOutputDeviceID, audioOutputProcID ); // call base class CSoundBase::Stop(); @@ -906,13 +1105,13 @@ int CSound::Init ( const int iNewPrefMonoBufferSize ) "select different input/output devices in your system settings." ); // try to set input buffer size - iActualMonoBufferSize = SetBufferSize ( audioInputDevice[lCurDev], true, iNewPrefMonoBufferSize ); + iActualMonoBufferSize = SetBufferSize ( CurrentAudioInputDeviceID, true, iNewPrefMonoBufferSize ); if ( iActualMonoBufferSize != static_cast ( iNewPrefMonoBufferSize ) ) { // try to set the input buffer size to the output so that we // have a matching pair - if ( SetBufferSize ( audioOutputDevice[lCurDev], false, iActualMonoBufferSize ) != iActualMonoBufferSize ) + if ( SetBufferSize ( CurrentAudioOutputDeviceID, false, iActualMonoBufferSize ) != iActualMonoBufferSize ) { throw CGenErr ( strErrBufSize ); } @@ -920,7 +1119,7 @@ int CSound::Init ( const int iNewPrefMonoBufferSize ) else { // try to set output buffer size - if ( SetBufferSize ( audioOutputDevice[lCurDev], false, iNewPrefMonoBufferSize ) != static_cast ( iNewPrefMonoBufferSize ) ) + if ( SetBufferSize ( CurrentAudioOutputDeviceID, false, iNewPrefMonoBufferSize ) != static_cast ( iNewPrefMonoBufferSize ) ) { throw CGenErr ( strErrBufSize ); } diff --git a/src/sound/coreaudio-mac/sound.h b/src/sound/coreaudio-mac/sound.h index 970c63718f..bcfd3e4093 100644 --- a/src/sound/coreaudio-mac/sound.h +++ b/src/sound/coreaudio-mac/sound.h @@ -82,6 +82,15 @@ class CSound : public CSoundBase virtual int GetLeftOutputChannel() override { return iSelOutputLeftChannel; } virtual int GetRightOutputChannel() override { return iSelOutputRightChannel; } + // CoreAudio manages the input and the output device separately, therefore we + // offer one device list per direction instead of all possible combinations + virtual bool IsInOutDevSelectionSeparate() const override { return true; } + virtual QStringList GetInputDevNames() override; + virtual QStringList GetOutputDevNames() override; + virtual QString GetInputDev() override; + virtual QString GetOutputDev() override; + virtual QString SetInOutDev ( const QString& strInDevName, const QString& strOutDevName ) override; + // MIDI functions virtual void EnableMIDI ( const bool bEnable ) override; virtual bool IsMIDIEnabled() const override; @@ -94,7 +103,6 @@ class CSound : public CSoundBase int iCoreAudioBufferSizeStereo; AudioDeviceID CurrentAudioInputDeviceID; AudioDeviceID CurrentAudioOutputDeviceID; - long lCurDev; int iNumInChan; int iNumInChanPlusAddChan; // includes additional "added" channels int iNumOutChan; @@ -118,12 +126,21 @@ class CSound : public CSoundBase CVector vecNumOutBufChan; protected: - virtual QString LoadAndInitializeDriver ( QString strDriverName, bool ) override; + virtual QString LoadAndInitializeDriver ( QString strDriverName, bool ) override; + virtual QVector LoadAndInitializeFirstValidDriver ( const bool bOpenDriverSetup = false ) override; - QString CheckDeviceCapabilities ( const int iDriverIdx ); + QString CheckInputDeviceCapabilities ( const int iInDevIdx ); + QString CheckOutputDeviceCapabilities ( const int iOutDevIdx ); + void ApplyDeviceSelection ( const int iInDevIdx, const int iOutDevIdx ); void UpdateChSelection(); void GetAvailableInOutDevices(); + // the input and the output device are selected separately but are stored in + // one single combined device name, see ComposeDevName()/SplitAndResolveDevName() + static QString ComposeDevName ( const QString& strInDevName, const QString& strOutDevName ); + bool SplitAndResolveDevName ( const QString& strDevName, int& iInDevIdx, int& iOutDevIdx ) const; + int FindDevIdx ( const QString strDevNames[], const long lNumDevices, const QString& strDevName ) const; + int CountChannels ( AudioDeviceID devID, bool isInput ); UInt32 SetBufferSize ( AudioDeviceID& audioDeviceID, const bool bIsInput, UInt32 iPrefBufferSize ); @@ -148,8 +165,20 @@ class CSound : public CSoundBase static void callbackMIDI ( const MIDIPacketList* pktlist, void* refCon, void* ); - AudioDeviceID audioInputDevice[MAX_NUMBER_SOUND_CARDS]; - AudioDeviceID audioOutputDevice[MAX_NUMBER_SOUND_CARDS]; + // available input and output devices (each list starts with the entry which + // follows the device selected as system default) + long lNumInDevs; + long lNumOutDevs; + QString strInputDeviceNames[MAX_NUMBER_SOUND_CARDS]; + QString strOutputDeviceNames[MAX_NUMBER_SOUND_CARDS]; + AudioDeviceID audioInputDevice[MAX_NUMBER_SOUND_CARDS]; + AudioDeviceID audioOutputDevice[MAX_NUMBER_SOUND_CARDS]; + + // names of the currently selected devices (the combined name is stored in + // strCurDevName of the base class) + QString strCurInDevName; + QString strCurOutDevName; + AudioDeviceIOProcID audioInputProcID; AudioDeviceIOProcID audioOutputProcID; From f3159ea12ecaf950aeb577e59ba2bf07d3346ab6 Mon Sep 17 00:00:00 2001 From: jshipman42 Date: Sun, 16 Aug 2026 03:19:10 -0400 Subject: [PATCH 4/6] macOS: only reset the channel mapping of the direction which changed Selecting a device resets the channel mapping to the first two available channels, which was correct as long as the input and the output device were always selected together. Now that both can be selected on their own, only reset the mapping of the direction whose device actually changed, so that changing the output device no longer discards the input channel mapping (and vice versa). The device notifications are registered per direction as well, but keyed on the device ID rather than on the direction: one device can serve both directions and CoreAudio then keeps one single registration for it, so removing it for one direction would also silence the notifications of the other one. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012i8guRg33BV5Prf7tqkkcH --- src/sound/coreaudio-mac/sound.cpp | 127 +++++++++++++++++++----------- src/sound/coreaudio-mac/sound.h | 1 + 2 files changed, 81 insertions(+), 47 deletions(-) diff --git a/src/sound/coreaudio-mac/sound.cpp b/src/sound/coreaudio-mac/sound.cpp index 28832e82fd..580c0e5ba5 100644 --- a/src/sound/coreaudio-mac/sound.cpp +++ b/src/sound/coreaudio-mac/sound.cpp @@ -481,80 +481,113 @@ QVector CSound::LoadAndInitializeFirstValidDriver ( const bool return vErrorList; } -void CSound::ApplyDeviceSelection ( const int iInDevIdx, const int iOutDevIdx ) +void CSound::SetDeviceNotifications ( const AudioDeviceID DeviceID, const bool bEnable ) { - const QString strNewDevName = ComposeDevName ( strInputDeviceNames[iInDevIdx], strOutputDeviceNames[iOutDevIdx] ); - - // nothing to do if the selected devices are already in use - if ( strCurDevName.compare ( strNewDevName ) == 0 ) - { - return; - } - AudioObjectPropertyAddress stPropertyAddress; stPropertyAddress.mElement = kAudioObjectPropertyElementMaster; stPropertyAddress.mScope = kAudioObjectPropertyScopeGlobal; - // unregister the callback functions if devices were already selected (a - // device ID of zero means that no device was registered so far) - if ( CurrentAudioInputDeviceID != 0 ) - { - stPropertyAddress.mSelector = kAudioHardwarePropertyDefaultOutputDevice; + const AudioObjectPropertySelector aSelectors[] = { kAudioDevicePropertyDeviceHasChanged, kAudioDevicePropertyDeviceIsAlive }; - AudioObjectRemovePropertyListener ( kAudioObjectSystemObject, &stPropertyAddress, deviceNotification, this ); + for ( const AudioObjectPropertySelector eSelector : aSelectors ) + { + stPropertyAddress.mSelector = eSelector; - stPropertyAddress.mSelector = kAudioHardwarePropertyDefaultInputDevice; + if ( bEnable ) + { + AudioObjectAddPropertyListener ( DeviceID, &stPropertyAddress, deviceNotification, this ); + } + else + { + AudioObjectRemovePropertyListener ( DeviceID, &stPropertyAddress, deviceNotification, this ); + } + } +} - AudioObjectRemovePropertyListener ( kAudioObjectSystemObject, &stPropertyAddress, deviceNotification, this ); +void CSound::ApplyDeviceSelection ( const int iInDevIdx, const int iOutDevIdx ) +{ + const AudioDeviceID NewAudioInputDeviceID = audioInputDevice[iInDevIdx]; + const AudioDeviceID NewAudioOutputDeviceID = audioOutputDevice[iOutDevIdx]; + const AudioDeviceID OldAudioInputDeviceID = CurrentAudioInputDeviceID; + const AudioDeviceID OldAudioOutputDeviceID = CurrentAudioOutputDeviceID; - stPropertyAddress.mSelector = kAudioDevicePropertyDeviceHasChanged; + const bool bInDevChanged = ( strCurInDevName.compare ( strInputDeviceNames[iInDevIdx] ) != 0 ); + const bool bOutDevChanged = ( strCurOutDevName.compare ( strOutputDeviceNames[iOutDevIdx] ) != 0 ); - AudioObjectRemovePropertyListener ( CurrentAudioOutputDeviceID, &stPropertyAddress, deviceNotification, this ); + // store the names of the selected devices + strCurInDevName = strInputDeviceNames[iInDevIdx]; + strCurOutDevName = strOutputDeviceNames[iOutDevIdx]; + strCurDevName = ComposeDevName ( strCurInDevName, strCurOutDevName ); - AudioObjectRemovePropertyListener ( CurrentAudioInputDeviceID, &stPropertyAddress, deviceNotification, this ); + if ( !bInDevChanged && !bOutDevChanged ) + { + // nothing else to do since the notifications are already registered + return; + } - stPropertyAddress.mSelector = kAudioDevicePropertyDeviceIsAlive; + // the system default device notifications only have to be registered once + const bool bRegisterSystemNotifications = ( OldAudioInputDeviceID == 0 ) && ( OldAudioOutputDeviceID == 0 ); - AudioObjectRemovePropertyListener ( CurrentAudioOutputDeviceID, &stPropertyAddress, deviceNotification, this ); + // store IDs of the selected devices + CurrentAudioInputDeviceID = NewAudioInputDeviceID; + CurrentAudioOutputDeviceID = NewAudioOutputDeviceID; - AudioObjectRemovePropertyListener ( CurrentAudioInputDeviceID, &stPropertyAddress, deviceNotification, this ); + // One device can serve both directions, in which case CoreAudio keeps one + // single registration for it. The notifications are therefore managed per + // device ID: they are only removed from a device which is no longer used by + // either direction and only added to a device which was not in use before. + if ( ( OldAudioInputDeviceID != 0 ) && ( OldAudioInputDeviceID != NewAudioInputDeviceID ) && ( OldAudioInputDeviceID != NewAudioOutputDeviceID ) ) + { + SetDeviceNotifications ( OldAudioInputDeviceID, false ); } - // store the selected devices - CurrentAudioInputDeviceID = audioInputDevice[iInDevIdx]; - CurrentAudioOutputDeviceID = audioOutputDevice[iOutDevIdx]; - strCurInDevName = strInputDeviceNames[iInDevIdx]; - strCurOutDevName = strOutputDeviceNames[iOutDevIdx]; - strCurDevName = strNewDevName; - - // setup callbacks for device property changes - stPropertyAddress.mSelector = kAudioDevicePropertyDeviceHasChanged; + if ( ( OldAudioOutputDeviceID != 0 ) && ( OldAudioOutputDeviceID != OldAudioInputDeviceID ) && + ( OldAudioOutputDeviceID != NewAudioInputDeviceID ) && ( OldAudioOutputDeviceID != NewAudioOutputDeviceID ) ) + { + SetDeviceNotifications ( OldAudioOutputDeviceID, false ); + } - AudioObjectAddPropertyListener ( CurrentAudioInputDeviceID, &stPropertyAddress, deviceNotification, this ); + if ( ( NewAudioInputDeviceID != OldAudioInputDeviceID ) && ( NewAudioInputDeviceID != OldAudioOutputDeviceID ) ) + { + SetDeviceNotifications ( NewAudioInputDeviceID, true ); + } - AudioObjectAddPropertyListener ( CurrentAudioOutputDeviceID, &stPropertyAddress, deviceNotification, this ); + if ( ( NewAudioOutputDeviceID != NewAudioInputDeviceID ) && ( NewAudioOutputDeviceID != OldAudioInputDeviceID ) && + ( NewAudioOutputDeviceID != OldAudioOutputDeviceID ) ) + { + SetDeviceNotifications ( NewAudioOutputDeviceID, true ); + } - stPropertyAddress.mSelector = kAudioDevicePropertyDeviceIsAlive; + if ( bRegisterSystemNotifications ) + { + AudioObjectPropertyAddress stPropertyAddress; - AudioObjectAddPropertyListener ( CurrentAudioInputDeviceID, &stPropertyAddress, deviceNotification, this ); + stPropertyAddress.mElement = kAudioObjectPropertyElementMaster; + stPropertyAddress.mScope = kAudioObjectPropertyScopeGlobal; - AudioObjectAddPropertyListener ( CurrentAudioOutputDeviceID, &stPropertyAddress, deviceNotification, this ); + stPropertyAddress.mSelector = kAudioHardwarePropertyDefaultOutputDevice; - stPropertyAddress.mSelector = kAudioHardwarePropertyDefaultOutputDevice; + AudioObjectAddPropertyListener ( kAudioObjectSystemObject, &stPropertyAddress, deviceNotification, this ); - AudioObjectAddPropertyListener ( kAudioObjectSystemObject, &stPropertyAddress, deviceNotification, this ); + stPropertyAddress.mSelector = kAudioHardwarePropertyDefaultInputDevice; - stPropertyAddress.mSelector = kAudioHardwarePropertyDefaultInputDevice; + AudioObjectAddPropertyListener ( kAudioObjectSystemObject, &stPropertyAddress, deviceNotification, this ); + } - AudioObjectAddPropertyListener ( kAudioObjectSystemObject, &stPropertyAddress, deviceNotification, this ); + // a device has changed, per definition we reset the channel mapping of that + // direction to the defaults (first two available channels) + if ( bInDevChanged ) + { + SetLeftInputChannel ( 0 ); + SetRightInputChannel ( 1 ); + } - // the device has changed, per definition we reset the channel - // mapping to the defaults (first two available channels) - SetLeftInputChannel ( 0 ); - SetRightInputChannel ( 1 ); - SetLeftOutputChannel ( 0 ); - SetRightOutputChannel ( 1 ); + if ( bOutDevChanged ) + { + SetLeftOutputChannel ( 0 ); + SetRightOutputChannel ( 1 ); + } } QString CSound::CheckInputDeviceCapabilities ( const int iInDevIdx ) diff --git a/src/sound/coreaudio-mac/sound.h b/src/sound/coreaudio-mac/sound.h index bcfd3e4093..aee2de692e 100644 --- a/src/sound/coreaudio-mac/sound.h +++ b/src/sound/coreaudio-mac/sound.h @@ -132,6 +132,7 @@ class CSound : public CSoundBase QString CheckInputDeviceCapabilities ( const int iInDevIdx ); QString CheckOutputDeviceCapabilities ( const int iOutDevIdx ); void ApplyDeviceSelection ( const int iInDevIdx, const int iOutDevIdx ); + void SetDeviceNotifications ( const AudioDeviceID DeviceID, const bool bEnable ); void UpdateChSelection(); void GetAvailableInOutDevices(); From 7f55fce9cc0666041bd84352fd3505a5428e01d0 Mon Sep 17 00:00:00 2001 From: jshipman42 Date: Sun, 16 Aug 2026 03:19:23 -0400 Subject: [PATCH 5/6] macOS: compare the device IDs when re-registering the notifications The check whether the selected device changed compared the device names, but the "System Default" entry follows whatever macOS has configured, so the device behind it can change while its name stays the same. Reloading the driver then kept the previous device IDs, and since the IO callback compares the device it is called for against them, it would discard the audio of the device it had just been started on. Note that this only covers the paths which reload the driver. A change of the macOS default device itself still only triggers RS_ONLY_RESTART, which does not reload, so the system default entry does not follow the change until the driver is reloaded for another reason. Fixing that is a change to the notification handling and out of scope here. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012i8guRg33BV5Prf7tqkkcH --- src/sound/coreaudio-mac/sound.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/sound/coreaudio-mac/sound.cpp b/src/sound/coreaudio-mac/sound.cpp index 580c0e5ba5..73c66ad974 100644 --- a/src/sound/coreaudio-mac/sound.cpp +++ b/src/sound/coreaudio-mac/sound.cpp @@ -512,10 +512,16 @@ void CSound::ApplyDeviceSelection ( const int iInDevIdx, const int iOutDevIdx ) const AudioDeviceID OldAudioInputDeviceID = CurrentAudioInputDeviceID; const AudioDeviceID OldAudioOutputDeviceID = CurrentAudioOutputDeviceID; - const bool bInDevChanged = ( strCurInDevName.compare ( strInputDeviceNames[iInDevIdx] ) != 0 ); - const bool bOutDevChanged = ( strCurOutDevName.compare ( strOutputDeviceNames[iOutDevIdx] ) != 0 ); - - // store the names of the selected devices + // compare the device IDs and not the device names: the device behind an + // entry can change while its name stays the same, i.e. for the system + // default entry when the driver is reloaded after the default device was + // changed in macOS + const bool bInDevChanged = ( NewAudioInputDeviceID != OldAudioInputDeviceID ); + const bool bOutDevChanged = ( NewAudioOutputDeviceID != OldAudioOutputDeviceID ); + + // store the names of the selected devices in any case since the same device + // may now be addressed by another entry (i.e. by its name instead of by the + // system default entry) strCurInDevName = strInputDeviceNames[iInDevIdx]; strCurOutDevName = strOutputDeviceNames[iOutDevIdx]; strCurDevName = ComposeDevName ( strCurInDevName, strCurOutDevName ); From 77762c0279c13f608f595812b9bca237ceea66f0 Mon Sep 17 00:00:00 2001 From: jshipman42 Date: Sun, 16 Aug 2026 04:37:21 -0400 Subject: [PATCH 6/6] Add John Shipman to the contributors list Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012i8guRg33BV5Prf7tqkkcH --- src/util.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/util.cpp b/src/util.cpp index 39efa10a16..2ae97a3f59 100644 --- a/src/util.cpp +++ b/src/util.cpp @@ -617,6 +617,7 @@ CAboutDlg::CAboutDlg ( QWidget* parent ) : CBaseDlg ( parent ) "

Thai Pangsakulyanont (dtinth)

" "

Peter Goderie (pgScorpio)

" "

Dan Garton (danryu)

" + "

John Shipman (jshipman42)

" "
" + tr ( "For details on the contributions check out the %1" ) .arg ( "" + tr ( "Github Contributors list" ) + "." ) );