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/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..73c66ad974 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,86 +394,183 @@ 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(); + + // 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; - // check if device is capable and if not the same device is used - if ( strStat.isEmpty() && ( strCurDevName.compare ( strDriverNames[iDriverIdx] ) != 0 ) ) + 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() ) { - stPropertyAddress.mElement = kAudioObjectPropertyElementMaster; - stPropertyAddress.mScope = kAudioObjectPropertyScopeGlobal; + iValidInDevIdx = iInDevIdx; + } + else + { + 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::SetDeviceNotifications ( const AudioDeviceID DeviceID, const bool bEnable ) +{ + AudioObjectPropertyAddress stPropertyAddress; - AudioObjectRemovePropertyListener ( audioInputDevice[lCurDev], &stPropertyAddress, deviceNotification, this ); + stPropertyAddress.mElement = kAudioObjectPropertyElementMaster; + stPropertyAddress.mScope = kAudioObjectPropertyScopeGlobal; - stPropertyAddress.mSelector = kAudioDevicePropertyDeviceIsAlive; + const AudioObjectPropertySelector aSelectors[] = { kAudioDevicePropertyDeviceHasChanged, kAudioDevicePropertyDeviceIsAlive }; - AudioObjectRemovePropertyListener ( audioOutputDevice[lCurDev], &stPropertyAddress, deviceNotification, this ); + for ( const AudioObjectPropertySelector eSelector : aSelectors ) + { + stPropertyAddress.mSelector = eSelector; - AudioObjectRemovePropertyListener ( audioInputDevice[lCurDev], &stPropertyAddress, deviceNotification, this ); + if ( bEnable ) + { + AudioObjectAddPropertyListener ( DeviceID, &stPropertyAddress, deviceNotification, this ); + } + else + { + AudioObjectRemovePropertyListener ( DeviceID, &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; + + // 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 ); + + if ( !bInDevChanged && !bOutDevChanged ) + { + // nothing else to do since the notifications are already registered + return; + } - // store ID of selected driver if initialization was successful - lCurDev = iDriverIdx; - CurrentAudioInputDeviceID = audioInputDevice[iDriverIdx]; - CurrentAudioOutputDeviceID = audioOutputDevice[iDriverIdx]; + // the system default device notifications only have to be registered once + const bool bRegisterSystemNotifications = ( OldAudioInputDeviceID == 0 ) && ( OldAudioOutputDeviceID == 0 ); - // register callbacks - stPropertyAddress.mElement = kAudioObjectPropertyElementMaster; - stPropertyAddress.mScope = kAudioObjectPropertyScopeGlobal; + // store IDs of the selected devices + CurrentAudioInputDeviceID = NewAudioInputDeviceID; + CurrentAudioOutputDeviceID = NewAudioOutputDeviceID; - // setup callbacks for device property changes - stPropertyAddress.mSelector = kAudioDevicePropertyDeviceHasChanged; + // 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 ); + } - AudioObjectAddPropertyListener ( audioInputDevice[lCurDev], &stPropertyAddress, deviceNotification, this ); + if ( ( OldAudioOutputDeviceID != 0 ) && ( OldAudioOutputDeviceID != OldAudioInputDeviceID ) && + ( OldAudioOutputDeviceID != NewAudioInputDeviceID ) && ( OldAudioOutputDeviceID != NewAudioOutputDeviceID ) ) + { + SetDeviceNotifications ( OldAudioOutputDeviceID, false ); + } - AudioObjectAddPropertyListener ( audioOutputDevice[lCurDev], &stPropertyAddress, deviceNotification, this ); + if ( ( NewAudioInputDeviceID != OldAudioInputDeviceID ) && ( NewAudioInputDeviceID != OldAudioOutputDeviceID ) ) + { + SetDeviceNotifications ( NewAudioInputDeviceID, true ); + } - stPropertyAddress.mSelector = kAudioDevicePropertyDeviceIsAlive; + if ( ( NewAudioOutputDeviceID != NewAudioInputDeviceID ) && ( NewAudioOutputDeviceID != OldAudioInputDeviceID ) && + ( NewAudioOutputDeviceID != OldAudioOutputDeviceID ) ) + { + SetDeviceNotifications ( NewAudioOutputDeviceID, true ); + } - AudioObjectAddPropertyListener ( audioInputDevice[lCurDev], &stPropertyAddress, deviceNotification, this ); + if ( bRegisterSystemNotifications ) + { + AudioObjectPropertyAddress stPropertyAddress; - AudioObjectAddPropertyListener ( audioOutputDevice[lCurDev], &stPropertyAddress, deviceNotification, this ); + stPropertyAddress.mElement = kAudioObjectPropertyElementMaster; + stPropertyAddress.mScope = kAudioObjectPropertyScopeGlobal; stPropertyAddress.mSelector = kAudioHardwarePropertyDefaultOutputDevice; @@ -367,30 +579,33 @@ QString CSound::LoadAndInitializeDriver ( QString strDriverName, bool ) 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) + // 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 ); + } + + if ( bOutDevChanged ) + { SetLeftOutputChannel ( 0 ); SetRightOutputChannel ( 1 ); - - // store the current name of the driver - strCurDevName = strDriverNames[iDriverIdx]; } - - return strStat; } -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 +613,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 +621,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 +630,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 +661,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 +680,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 +697,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 +808,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 +825,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 +948,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 +963,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 +1144,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 +1158,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..aee2de692e 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,22 @@ 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 SetDeviceNotifications ( const AudioDeviceID DeviceID, const bool bEnable ); 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 +166,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; 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..424f048871 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 { @@ -110,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 ) {} @@ -157,11 +179,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 ) { 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" ) + "." ) );