diff --git a/docs/JSON-RPC.md b/docs/JSON-RPC.md index 84271b66a8..7cf7a92b3e 100644 --- a/docs/JSON-RPC.md +++ b/docs/JSON-RPC.md @@ -188,6 +188,40 @@ Results: | result.clients | array | The client list. See jamulusclient/clientListReceived for the format. | +### jamulusclient/getMidiDevices + +Returns a list of available MIDI input devices. + +Parameters: + +| Name | Type | Description | +| --- | --- | --- | +| params | object | No parameters (empty object). | + +Results: + +| Name | Type | Description | +| --- | --- | --- | +| result | array | Array of MIDI device name strings. | + + +### jamulusclient/getMidiSettings + +Returns all MIDI controller settings. + +Parameters: + +| Name | Type | Description | +| --- | --- | --- | +| params | object | No parameters (empty object). | + +Results: + +| Name | Type | Description | +| --- | --- | --- | +| result | object | MIDI settings object. | + + ### jamulusclient/pollServerList Request list of servers in a directory. @@ -240,6 +274,23 @@ Results: | result | string | Always "ok". | +### jamulusclient/setMidiSettings + +Sets one or more MIDI controller settings. + +Parameters: + +| Name | Type | Description | +| --- | --- | --- | +| params | object | Any subset of MIDI settings fields to set. | + +Results: + +| Name | Type | Description | +| --- | --- | --- | +| result | string | Always "ok". | + + ### jamulusclient/setName Sets your name. diff --git a/src/audiomixerboard.cpp b/src/audiomixerboard.cpp index 854ea218ec..0faddcb3b8 100644 --- a/src/audiomixerboard.cpp +++ b/src/audiomixerboard.cpp @@ -1334,6 +1334,9 @@ void CAudioMixerBoard::ApplyNewConClientList ( CVector& vecChanInf } Mutex.unlock(); // release mutex + // Ensure MIDI state is applied to faders during the connection process + SetMIDICtrlUsed ( pSettings->bUseMIDIController ); + // sort the channels according to the selected sorting type ChangeFaderOrder ( eChSortType ); diff --git a/src/client.cpp b/src/client.cpp index 1046d3206b..6b81c596ea 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -23,19 +23,21 @@ \******************************************************************************/ #include "client.h" +#include "settings.h" #include "util.h" /* Implementation *************************************************************/ CClient::CClient ( const quint16 iPortNumber, const quint16 iQosNumber, const QString& strConnOnStartupAddress, - const QString& strMIDISetup, const bool bNoAutoJackConnect, const QString& strNClientName, const bool bNEnableIPv6, const bool bNMuteMeInPersonalMix ) : ChannelInfo(), strClientName ( strNClientName ), + pSignalHandler ( CSignalHandler::getSingletonP() ), + pSettings ( nullptr ), Channel ( false ), /* we need a client channel -> "false" */ CurOpusEncoder ( nullptr ), CurOpusDecoder ( nullptr ), @@ -49,7 +51,7 @@ CClient::CClient ( const quint16 iPortNumber, bMuteOutStream ( false ), fMuteOutStreamGain ( 1.0f ), Socket ( &Channel, iPortNumber, iQosNumber, "", bNEnableIPv6 ), - Sound ( AudioCallback, this, strMIDISetup, bNoAutoJackConnect, strNClientName ), + Sound ( AudioCallback, this, bNoAutoJackConnect, strNClientName ), iAudioInFader ( AUD_FADER_IN_MIDDLE ), bReverbOnLeftChan ( false ), iReverbLevel ( 0 ), @@ -68,8 +70,7 @@ CClient::CClient ( const quint16 iPortNumber, bJitterBufferOK ( true ), bEnableIPv6 ( bNEnableIPv6 ), bMuteMeInPersonalMix ( bNMuteMeInPersonalMix ), - iServerSockBufNumFrames ( DEF_NET_BUF_SIZE_NUM_BL ), - pSignalHandler ( CSignalHandler::getSingletonP() ) + iServerSockBufNumFrames ( DEF_NET_BUF_SIZE_NUM_BL ) { int iOpusError; @@ -173,6 +174,8 @@ CClient::CClient ( const quint16 iPortNumber, QObject::connect ( pSignalHandler, &CSignalHandler::HandledSignal, this, &CClient::OnHandledSignal ); + QObject::connect ( &Sound, &CSoundBase::MidiCCReceived, this, [this] ( int ccNumber ) { emit MidiCCReceived ( ccNumber ); } ); + // start timer so that elapsed time works PreciseTime.start(); @@ -193,6 +196,29 @@ CClient::CClient ( const quint16 iPortNumber, } } +// MIDI setup will be handled after settings are assigned +void CClient::SetSettings ( CClientSettings* settings ) +{ + pSettings = settings; + + // Apply MIDI settings + Sound.SetCtrlMIDIChannel ( pSettings->iMidiChannel ); + Sound.SetMIDIControllerMapping ( pSettings->iMidiFaderOffset, + pSettings->bMidiFaderEnabled ? pSettings->iMidiFaderCount : 0, + pSettings->iMidiPanOffset, + pSettings->bMidiPanEnabled ? pSettings->iMidiPanCount : 0, + pSettings->iMidiSoloOffset, + pSettings->bMidiSoloEnabled ? pSettings->iMidiSoloCount : 0, + pSettings->iMidiMuteOffset, + pSettings->bMidiMuteEnabled ? pSettings->iMidiMuteCount : 0, + pSettings->bMidiMuteMyselfEnabled ? pSettings->iMidiMuteMyself : 0 ); + if ( !pSettings->strMidiDevice.isEmpty() ) + { + Sound.SetMIDIDevice ( pSettings->strMidiDevice ); + } + Sound.EnableMIDI ( pSettings->bUseMIDIController ); +} + CClient::~CClient() { // if we were running, stop sound device @@ -1547,6 +1573,8 @@ void CClient::FreeClientChannel ( const int iServerChannelID ) */ } +void CClient::OnMidiCCReceived ( int ccNumber ) { emit MidiCCReceived ( ccNumber ); } + // find, and optionally create, a client channel for the supplied server channel ID // returns a client channel ID or INVALID_INDEX int CClient::FindClientChannel ( const int iServerChannelID, const bool bCreateIfNew ) diff --git a/src/client.h b/src/client.h index 8f266a371a..6139ef599e 100644 --- a/src/client.h +++ b/src/client.h @@ -119,6 +119,8 @@ class CClientChannel // can store here other information about an active channel }; +class CClientSettings; + class CClient : public QObject { Q_OBJECT @@ -127,7 +129,6 @@ class CClient : public QObject CClient ( const quint16 iPortNumber, const quint16 iQosNumber, const QString& strConnOnStartupAddress, - const QString& strMIDISetup, const bool bNoAutoJackConnect, const QString& strNClientName, const bool bNEnableIPv6, @@ -293,11 +294,25 @@ class CClient : public QObject CProtocol* getConnLessProtocol() { return &ConnLessProtocol; } //### TODO: END ###// + // MIDI control + void EnableMIDI ( bool bEnable ) { Sound.EnableMIDI ( bEnable ); } + bool IsMIDIEnabled() const { return Sound.IsMIDIEnabled(); } + QStringList GetMIDIDevNames() { return Sound.GetMIDIDevNames(); } + QString GetMIDIDevice() { return Sound.GetMIDIDevice(); } + void SetMIDIDevice ( const QString& strDevice ) { Sound.SetMIDIDevice ( strDevice ); } + // settings CChannelCoreInfo ChannelInfo; QString strClientName; +public: + void SetSettings ( CClientSettings* settings ); + protected: + // Signal handler must be declared before pSettings for correct init order + CSignalHandler* pSignalHandler; + // Pointer to settings for MIDI and other config + CClientSettings* pSettings; // callback function must be static, otherwise it does not work static void AudioCallback ( CVector& psData, void* arg ); @@ -407,8 +422,6 @@ class CClient : public QObject int maxGainOrPanId; int iCurPingTime; - CSignalHandler* pSignalHandler; - protected slots: void OnHandledSignal ( int sigNum ); void OnSendProtMessage ( CVector vecMessage ); @@ -473,4 +486,8 @@ protected slots: void ControllerInFaderIsSolo ( int iChannelIdx, bool bIsSolo ); void ControllerInFaderIsMute ( int iChannelIdx, bool bIsMute ); void ControllerInMuteMyself ( bool bMute ); + void MidiCCReceived ( int ccNumber ); + +private slots: + void OnMidiCCReceived ( int ccNumber ); }; diff --git a/src/clientdlg.cpp b/src/clientdlg.cpp index cd678deacb..6063547896 100644 --- a/src/clientdlg.cpp +++ b/src/clientdlg.cpp @@ -29,7 +29,6 @@ CClientDlg::CClientDlg ( CClient* pNCliP, CClientSettings* pNSetP, const QString& strConnOnStartupAddress, - const QString& strMIDISetup, const bool bNewShowComplRegConnList, const bool bShowAnalyzerConsole, const bool bMuteStream, @@ -220,7 +219,7 @@ CClientDlg::CClientDlg ( CClient* pNCliP, MainMixerBoard->SetNumMixerPanelRows ( pSettings->iNumMixerPanelRows ); // Pass through flag for MIDICtrlUsed - MainMixerBoard->SetMIDICtrlUsed ( !strMIDISetup.isEmpty() ); + MainMixerBoard->SetMIDICtrlUsed ( pSettings->bUseMIDIController ); // reset mixer board MainMixerBoard->HideAll(); @@ -401,6 +400,12 @@ CClientDlg::CClientDlg ( CClient* pNCliP, pSettingsMenu->addAction ( tr ( "A&dvanced Settings..." ), this, SLOT ( OnOpenAdvancedSettings() ), QKeySequence ( Qt::CTRL + Qt::Key_D ) ); + pSettingsMenu->addAction ( + tr ( "&MIDI Control Settings..." ), + this, + [this] { ShowGeneralSettings ( SETTING_TAB_MIDI ); }, + QKeySequence ( Qt::CTRL + Qt::Key_M ) ); + // Main menu bar ----------------------------------------------------------- QMenuBar* pMenu = new QMenuBar ( this ); @@ -536,6 +541,8 @@ CClientDlg::CClientDlg ( CClient* pNCliP, QObject::connect ( &ClientSettingsDlg, &CClientSettingsDlg::NumMixerPanelRowsChanged, this, &CClientDlg::OnNumMixerPanelRowsChanged ); + QObject::connect ( &ClientSettingsDlg, &CClientSettingsDlg::MIDIControllerUsageChanged, this, &CClientDlg::OnMIDIControllerUsageChanged ); + QObject::connect ( this, &CClientDlg::SendTabChange, &ClientSettingsDlg, &CClientSettingsDlg::OnMakeTabChange ); QObject::connect ( MainMixerBoard, &CAudioMixerBoard::ChangeChanGain, this, &CClientDlg::OnChangeChanGain ); @@ -1526,3 +1533,13 @@ void CClientDlg::SetPingTime ( const int iPingTime, const int iOverallDelayMs, c // set current LED status ledDelay->SetLight ( eOverallDelayLEDColor ); } + +// OnOpenMidiSettings slot removed; lambda is used in menu action +void CClientDlg::OnMIDIControllerUsageChanged ( bool bEnabled ) +{ + // Update the mixer board's MIDI flag to trigger proper user numbering display + MainMixerBoard->SetMIDICtrlUsed ( bEnabled ); + + // Enable/disable runtime MIDI via the sound interface through the public CClient interface + pClient->EnableMIDI ( bEnabled ); +} diff --git a/src/clientdlg.h b/src/clientdlg.h index e26acb05f8..a5d327676a 100644 --- a/src/clientdlg.h +++ b/src/clientdlg.h @@ -80,7 +80,6 @@ class CClientDlg : public CBaseDlg, private Ui_CClientDlgBase CClientDlg ( CClient* pNCliP, CClientSettings* pNSetP, const QString& strConnOnStartupAddress, - const QString& strMIDISetup, const bool bNewShowComplRegConnList, const bool bShowAnalyzerConsole, const bool bMuteStream, @@ -246,6 +245,8 @@ public slots: void accept() { close(); } // introduced by pljones + void OnMIDIControllerUsageChanged ( bool bEnabled ); + signals: void SendTabChange ( int iTabIdx ); }; diff --git a/src/clientrpc.cpp b/src/clientrpc.cpp index f2c3f53cc3..e01593f40b 100644 --- a/src/clientrpc.cpp +++ b/src/clientrpc.cpp @@ -25,7 +25,9 @@ #include "clientrpc.h" -CClientRpc::CClientRpc ( CClient* pClient, CRpcServer* pRpcServer, QObject* parent ) : QObject ( parent ) +CClientRpc::CClientRpc ( CClient* pClient, CClientSettings* pSettings, CRpcServer* pRpcServer, QObject* parent ) : + QObject ( parent ), + m_pSettings ( pSettings ) { /// @rpc_notification jamulusclient/chatTextReceived /// @brief Emitted when a chat text is received. @@ -355,6 +357,112 @@ CClientRpc::CClientRpc ( CClient* pClient, CRpcServer* pRpcServer, QObject* pare pClient->SetControllerInFaderLevel ( jsonChannelIndex.toInt(), jsonLevel.toInt() ); response["result"] = "ok"; } ); + + /// @rpc_method jamulusclient/getMidiSettings + /// @brief Returns all MIDI controller settings. + /// @param {object} params - No parameters (empty object). + /// @result {object} result - MIDI settings object. + pRpcServer->HandleMethod ( "jamulusclient/getMidiSettings", [=] ( const QJsonObject& params, QJsonObject& response ) { + QJsonObject jsonMidiParams{ { "bUseMIDIController", m_pSettings->bUseMIDIController }, + { "midiDevice", m_pSettings->strMidiDevice }, + { "midiChannel", m_pSettings->iMidiChannel }, + { "midiMuteMyself", m_pSettings->iMidiMuteMyself }, + { "midiFaderOffset", m_pSettings->iMidiFaderOffset }, + { "midiFaderCount", m_pSettings->iMidiFaderCount }, + { "midiPanOffset", m_pSettings->iMidiPanOffset }, + { "midiPanCount", m_pSettings->iMidiPanCount }, + { "midiSoloOffset", m_pSettings->iMidiSoloOffset }, + { "midiSoloCount", m_pSettings->iMidiSoloCount }, + { "midiMuteOffset", m_pSettings->iMidiMuteOffset }, + { "midiMuteCount", m_pSettings->iMidiMuteCount } }; + response["result"] = jsonMidiParams; + Q_UNUSED ( params ); + } ); + + /// @rpc_method jamulusclient/setMidiSettings + /// @brief Sets one or more MIDI controller settings. + /// @param {object} params - Any subset of MIDI settings fields to set. + /// @result {string} result - Always "ok". + pRpcServer->HandleMethod ( "jamulusclient/setMidiSettings", [=] ( const QJsonObject& params, QJsonObject& response ) { + bool bPreviousMIDIState = m_pSettings->bUseMIDIController; + + QHash> setters = { + { "bUseMIDIController", [this] ( const QJsonValue& v ) { m_pSettings->bUseMIDIController = v.toBool(); } }, + { "midiDevice", [this] ( const QJsonValue& v ) { m_pSettings->strMidiDevice = v.toString(); } }, + { "midiChannel", [this] ( const QJsonValue& v ) { m_pSettings->iMidiChannel = v.toInt(); } }, + { "midiMuteMyself", [this] ( const QJsonValue& v ) { m_pSettings->iMidiMuteMyself = v.toInt(); } }, + { "midiFaderOffset", [this] ( const QJsonValue& v ) { m_pSettings->iMidiFaderOffset = v.toInt(); } }, + { "midiFaderCount", [this] ( const QJsonValue& v ) { m_pSettings->iMidiFaderCount = v.toInt(); } }, + { "midiPanOffset", [this] ( const QJsonValue& v ) { m_pSettings->iMidiPanOffset = v.toInt(); } }, + { "midiPanCount", [this] ( const QJsonValue& v ) { m_pSettings->iMidiPanCount = v.toInt(); } }, + { "midiSoloOffset", [this] ( const QJsonValue& v ) { m_pSettings->iMidiSoloOffset = v.toInt(); } }, + { "midiSoloCount", [this] ( const QJsonValue& v ) { m_pSettings->iMidiSoloCount = v.toInt(); } }, + { "midiMuteOffset", [this] ( const QJsonValue& v ) { m_pSettings->iMidiMuteOffset = v.toInt(); } }, + { "midiMuteCount", [this] ( const QJsonValue& v ) { m_pSettings->iMidiMuteCount = v.toInt(); } } }; + + for ( auto it = setters.constBegin(); it != setters.constEnd(); ++it ) + { + if ( params.contains ( it.key() ) ) + { + it.value() ( params[it.key()] ); + } + } + + // If midiDevice was changed and MIDI is currently enabled, restart MIDI to reconnect + bool bDeviceChanged = params.contains ( "midiDevice" ); + if ( bDeviceChanged && m_pSettings->bUseMIDIController && pClient->IsMIDIEnabled() ) + { + // Disable MIDI + pClient->EnableMIDI ( false ); + + // Set the new device + pClient->SetMIDIDevice ( m_pSettings->strMidiDevice ); + + // Re-enable MIDI + pClient->EnableMIDI ( true ); + + // Check if reconnection was successful + if ( !pClient->IsMIDIEnabled() ) + { + response["error"] = CRpcServer::CreateJsonRpcError ( 1, "Failed to connect to MIDI device" ); + return; + } + } + else if ( bDeviceChanged ) + { + // Just update the device setting for next time MIDI is enabled + pClient->SetMIDIDevice ( m_pSettings->strMidiDevice ); + } + + // Apply other settings to actually enable/disable MIDI + pClient->SetSettings ( m_pSettings ); + + // Check if MIDI was requested but failed to enable + if ( m_pSettings->bUseMIDIController && !pClient->IsMIDIEnabled() ) + { + // Restore previous state on failure + m_pSettings->bUseMIDIController = bPreviousMIDIState; + response["error"] = CRpcServer::CreateJsonRpcError ( 1, "Failed to open MIDI port" ); + return; + } + + response["result"] = "ok"; + } ); + + /// @rpc_method jamulusclient/getMidiDevices + /// @brief Returns a list of available MIDI input devices. + /// @param {object} params - No parameters (empty object). + /// @result {array} result - Array of MIDI device name strings. + pRpcServer->HandleMethod ( "jamulusclient/getMidiDevices", [=] ( const QJsonObject& params, QJsonObject& response ) { + QStringList deviceNames = pClient->GetMIDIDevNames(); + QJsonArray jsonDevices; + for ( const QString& deviceName : deviceNames ) + { + jsonDevices.append ( deviceName ); + } + response["result"] = jsonDevices; + Q_UNUSED ( params ); + } ); } QJsonValue CClientRpc::SerializeSkillLevel ( ESkillLevel eSkillLevel ) diff --git a/src/clientrpc.h b/src/clientrpc.h index b2f7d9ddd4..1483696fcf 100644 --- a/src/clientrpc.h +++ b/src/clientrpc.h @@ -28,6 +28,7 @@ #include "client.h" #include "util.h" #include "rpcserver.h" +#include "settings.h" /* Classes ********************************************************************/ class CClientRpc : public QObject @@ -35,9 +36,10 @@ class CClientRpc : public QObject Q_OBJECT public: - CClientRpc ( CClient* pClient, CRpcServer* pRpcServer, QObject* parent = nullptr ); + CClientRpc ( CClient* pClient, CClientSettings* pSettings, CRpcServer* pRpcServer, QObject* parent = nullptr ); private: + CClientSettings* m_pSettings; QJsonArray arrStoredChanInfo; static QJsonValue SerializeSkillLevel ( ESkillLevel skillLevel ); }; diff --git a/src/clientsettingsdlg.cpp b/src/clientsettingsdlg.cpp index 703d9ef0fe..3a533bfe18 100644 --- a/src/clientsettingsdlg.cpp +++ b/src/clientsettingsdlg.cpp @@ -28,7 +28,8 @@ CClientSettingsDlg::CClientSettingsDlg ( CClient* pNCliP, CClientSettings* pNSetP, QWidget* parent ) : CBaseDlg ( parent, Qt::Window ), // use Qt::Window to get min/max window buttons pClient ( pNCliP ), - pSettings ( pNSetP ) + pSettings ( pNSetP ), + midiLearnTarget ( None ) { setupUi ( this ); @@ -397,6 +398,57 @@ CClientSettingsDlg::CClientSettingsDlg ( CClient* pNCliP, CClientSettings* pNSet "A second sound device may be required to hear the alerts." ) ); chbAudioAlerts->setAccessibleName ( tr ( "Audio Alerts check box" ) ); + // MIDI settings + chbUseMIDIController->setWhatsThis ( tr ( "Enable/disable MIDI-in port" ) ); + chbUseMIDIController->setAccessibleName ( tr ( "MIDI-in port check box" ) ); + +#if defined( WITH_JACK ) + lblMidiDevice->setWhatsThis ( tr ( "Select which MIDI output port to connect to. " + "Jamulus will automatically connect its MIDI input port to the selected device when enabled." + "You can also use your connection manager of choice to manually change connections." ) ); +#elif defined( __APPLE__ ) + lblMidiDevice->setWhatsThis ( tr ( "Select which MIDI source to connect to. " + "Jamulus will automatically connect its MIDI input port to the selected device when enabled." + "You can also use Audio MIDI Setup to manually change connections." ) ); +#else + lblMidiDevice->setWhatsThis ( tr ( "Select which MIDI input device(s) Jamulus should listen to. " + "Select 'All Devices' to receive MIDI from all connected devices, or choose a specific device." ) ); +#endif + cbxMidiDevice->setAccessibleName ( tr ( "MIDI input device combo box" ) ); + + QString strMidiSettings = "" + tr ( "MIDI controller settings" ) + ": " + + tr ( "There is one global MIDI channel parameter (0-16) and two parameters you can set " + "for each item controlled: First MIDI CC and consecutive CC numbers (count). First set the " + "channel you want Jamulus to listen on (0 for all channels). Then, for each item " + "you want to control (volume fader, pan, solo, mute), set the first MIDI CC (CC number " + "to start from) and number of consecutive CC numbers (count). There is one " + "exception that does not require establishing consecutive CC numbers which is " + "the “Mute Myself” parameter - it only requires a single CC number as it is only " + "applied to one’s own audio stream." ) + + "
" + + tr ( "You can either type in the MIDI CC values or use the \"Learn\" button: click on " + "\"Learn\", actuate the fader/knob/button on your MIDI controller, and the MIDI CC " + "number will be detected and saved." ); + + lblChannel->setWhatsThis ( strMidiSettings ); + grbMidiMuteMyself->setWhatsThis ( strMidiSettings ); + grbMidiFader->setWhatsThis ( strMidiSettings ); + grbMidiPan->setWhatsThis ( strMidiSettings ); + grbMidiSolo->setWhatsThis ( strMidiSettings ); + grbMidiMute->setWhatsThis ( strMidiSettings ); + + cbxChannel->setAccessibleName ( tr ( "MIDI channel combo box" ) ); + spnMuteMyself->setAccessibleName ( tr ( "Mute Myself MIDI CC number spin box" ) ); + spnFaderOffset->setAccessibleName ( tr ( "Fader offset spin box" ) ); + spnPanOffset->setAccessibleName ( tr ( "Pan offset spin box" ) ); + spnSoloOffset->setAccessibleName ( tr ( "Solo offset spin box" ) ); + spnMuteOffset->setAccessibleName ( tr ( "Mute offset spin box" ) ); + butLearnMuteMyself->setAccessibleName ( tr ( "Mute Myself MIDI learn button" ) ); + butLearnFaderOffset->setAccessibleName ( tr ( "Fader offset MIDI learn button" ) ); + butLearnPanOffset->setAccessibleName ( tr ( "Pan offset MIDI learn button" ) ); + butLearnSoloOffset->setAccessibleName ( tr ( "Solo offset MIDI learn button" ) ); + butLearnMuteOffset->setAccessibleName ( tr ( "Mute offset MIDI learn button" ) ); + // init driver button #if defined( _WIN32 ) && !defined( WITH_JACK ) butDriverSetup->setText ( tr ( "ASIO Device Settings" ) ); @@ -746,6 +798,126 @@ CClientSettingsDlg::CClientSettingsDlg ( CClient* pNCliP, CClientSettings* pNSet QObject::connect ( pcbxSkill, static_cast ( &QComboBox::activated ), this, &CClientSettingsDlg::OnSkillActivated ); + // MIDI tab + + struct MidiSpinBoxMapping + { + QSpinBox* spinBox; + int CClientSettings::*member; + }; + + const MidiSpinBoxMapping midiMappings[] = { { spnMuteMyself, &CClientSettings::iMidiMuteMyself }, + { spnFaderOffset, &CClientSettings::iMidiFaderOffset }, + { spnFaderCount, &CClientSettings::iMidiFaderCount }, + { spnPanOffset, &CClientSettings::iMidiPanOffset }, + { spnPanCount, &CClientSettings::iMidiPanCount }, + { spnSoloOffset, &CClientSettings::iMidiSoloOffset }, + { spnSoloCount, &CClientSettings::iMidiSoloCount }, + { spnMuteOffset, &CClientSettings::iMidiMuteOffset }, + { spnMuteCount, &CClientSettings::iMidiMuteCount } }; + + for ( const MidiSpinBoxMapping& mapping : midiMappings ) + { + QObject::connect ( mapping.spinBox, static_cast ( &QSpinBox::valueChanged ), this, [this, mapping] ( int v ) { + pSettings->*( mapping.member ) = v; + pClient->SetSettings ( pSettings ); + } ); + } + + // Connect MIDI channel combobox + QObject::connect ( cbxChannel, static_cast ( &QComboBox::currentIndexChanged ), this, [this] ( int index ) { + pSettings->iMidiChannel = index; + pClient->SetSettings ( pSettings ); + } ); + + // Connect groupbox enable/disable checkboxes + QObject::connect ( grbMidiFader, &QGroupBox::toggled, this, [this] ( bool checked ) { + pSettings->bMidiFaderEnabled = checked; + pClient->SetSettings ( pSettings ); + butLearnFaderOffset->setEnabled ( checked ); + } ); + + QObject::connect ( grbMidiPan, &QGroupBox::toggled, this, [this] ( bool checked ) { + pSettings->bMidiPanEnabled = checked; + pClient->SetSettings ( pSettings ); + butLearnPanOffset->setEnabled ( checked ); + } ); + + QObject::connect ( grbMidiSolo, &QGroupBox::toggled, this, [this] ( bool checked ) { + pSettings->bMidiSoloEnabled = checked; + pClient->SetSettings ( pSettings ); + butLearnSoloOffset->setEnabled ( checked ); + } ); + + QObject::connect ( grbMidiMute, &QGroupBox::toggled, this, [this] ( bool checked ) { + pSettings->bMidiMuteEnabled = checked; + pClient->SetSettings ( pSettings ); + butLearnMuteOffset->setEnabled ( checked ); + } ); + + QObject::connect ( grbMidiMuteMyself, &QGroupBox::toggled, this, [this] ( bool checked ) { + pSettings->bMidiMuteMyselfEnabled = checked; + pClient->SetSettings ( pSettings ); + butLearnMuteMyself->setEnabled ( checked ); + } ); + + QObject::connect ( chbUseMIDIController, &QCheckBox::toggled, this, [this] ( bool checked ) { + pSettings->bUseMIDIController = checked; + pClient->SetSettings ( pSettings ); + + // Check if MIDI was actually enabled successfully + // Note: On Windows, IsMIDIEnabled() returns false if no devices found. + // On Linux/macOS (Jack/CoreAudio), MIDI ports can be created even without devices, + // so IsMIDIEnabled() should always return true when checked. + if ( checked && !pClient->IsMIDIEnabled() ) + { +#if defined( _WIN32 ) && !defined( WITH_JACK ) + // On Windows, MIDI port creation requires actual devices + // MIDI failed to enable - uncheck the box and update settings + pSettings->bUseMIDIController = false; + chbUseMIDIController->setChecked ( false ); + SetMIDIControlsEnabled ( false ); + QMessageBox::warning ( this, + tr ( "Could not open MIDI port" ), + tr ( "No MIDI devices found. Please connect a MIDI device and try again." ) ); +#else + // On Linux/macOS, this shouldn't happen, but handle it anyway + pSettings->bUseMIDIController = false; + chbUseMIDIController->setChecked ( false ); + SetMIDIControlsEnabled ( false ); + QMessageBox::warning ( this, tr ( "Could not open MIDI port" ), tr ( "Please check your OS configuration." ) ); +#endif + } + else + { + SetMIDIControlsEnabled ( checked ); + emit MIDIControllerUsageChanged ( pSettings->bUseMIDIController ); + } + } ); + + // MIDI Device combo box connection + QObject::connect ( cbxMidiDevice, + static_cast ( &QComboBox::activated ), + this, + &CClientSettingsDlg::OnMidiDeviceActivated ); + + // Install event filter to refresh device list when dropdown is opened + cbxMidiDevice->installEventFilter ( this ); + + // MIDI Learn buttons + midiLearnButtons[0] = butLearnMuteMyself; + midiLearnButtons[1] = butLearnFaderOffset; + midiLearnButtons[2] = butLearnPanOffset; + midiLearnButtons[3] = butLearnSoloOffset; + midiLearnButtons[4] = butLearnMuteOffset; + + for ( QPushButton* button : midiLearnButtons ) + { + QObject::connect ( button, &QPushButton::clicked, this, &CClientSettingsDlg::OnLearnButtonClicked ); + } + + QObject::connect ( pClient, &CClient::MidiCCReceived, this, &CClientSettingsDlg::OnMidiCCReceived ); + QObject::connect ( tabSettings, &QTabWidget::currentChanged, this, &CClientSettingsDlg::OnTabChanged ); tabSettings->setCurrentIndex ( pSettings->iSettingsTab ); @@ -755,7 +927,7 @@ CClientSettingsDlg::CClientSettingsDlg ( CClient* pNCliP, CClientSettings* pNSet TimerStatus.start ( DISPLAY_UPDATE_TIME ); } -void CClientSettingsDlg::showEvent ( QShowEvent* ) +void CClientSettingsDlg::showEvent ( QShowEvent* event ) { UpdateDisplay(); UpdateDirectoryComboBox(); @@ -774,6 +946,75 @@ void CClientSettingsDlg::showEvent ( QShowEvent* ) // select the skill level pcbxSkill->setCurrentIndex ( pcbxSkill->findData ( static_cast ( pClient->ChannelInfo.eSkillLevel ) ) ); + + // MIDI tab: set widgets from settings + cbxChannel->setCurrentIndex ( pSettings->iMidiChannel ); + spnMuteMyself->setValue ( pSettings->iMidiMuteMyself ); + spnFaderOffset->setValue ( pSettings->iMidiFaderOffset ); + spnFaderCount->setValue ( pSettings->iMidiFaderCount ); + spnPanOffset->setValue ( pSettings->iMidiPanOffset ); + spnPanCount->setValue ( pSettings->iMidiPanCount ); + spnSoloOffset->setValue ( pSettings->iMidiSoloOffset ); + spnSoloCount->setValue ( pSettings->iMidiSoloCount ); + spnMuteOffset->setValue ( pSettings->iMidiMuteOffset ); + spnMuteCount->setValue ( pSettings->iMidiMuteCount ); + chbUseMIDIController->setChecked ( pSettings->bUseMIDIController ); + + // Initialize groupbox checked states + grbMidiFader->setChecked ( pSettings->bMidiFaderEnabled ); + grbMidiPan->setChecked ( pSettings->bMidiPanEnabled ); + grbMidiSolo->setChecked ( pSettings->bMidiSoloEnabled ); + grbMidiMute->setChecked ( pSettings->bMidiMuteEnabled ); + grbMidiMuteMyself->setChecked ( pSettings->bMidiMuteMyselfEnabled ); + + // Initialize learn button states based on groupbox states + butLearnFaderOffset->setEnabled ( pSettings->bMidiFaderEnabled ); + butLearnPanOffset->setEnabled ( pSettings->bMidiPanEnabled ); + butLearnSoloOffset->setEnabled ( pSettings->bMidiSoloEnabled ); + butLearnMuteOffset->setEnabled ( pSettings->bMidiMuteEnabled ); + butLearnMuteMyself->setEnabled ( pSettings->bMidiMuteMyselfEnabled ); + + // Update MIDI device combo box + UpdateMIDIDeviceSelection(); + + // Check if MIDI is actually enabled (might have failed to open port) + // Note: On Windows, failure will occur if no MIDI devices are found. + // On Linux/macOS, MIDI ports are created regardless of device availability. + if ( pSettings->bUseMIDIController && !pClient->IsMIDIEnabled() ) + { +#if defined( _WIN32 ) && !defined( WITH_JACK ) + // On Windows, MIDI port creation requires actual devices + // If MIDI was requested but no devices found - uncheck and disable + pSettings->bUseMIDIController = false; + chbUseMIDIController->setChecked ( false ); +#else + // On Linux/macOS, this shouldn't happen, but handle it anyway + pSettings->bUseMIDIController = false; + chbUseMIDIController->setChecked ( false ); +#endif + } + + SetMIDIControlsEnabled ( chbUseMIDIController->isChecked() ); + + // Signal to propagate MIDI state at startup + emit MIDIControllerUsageChanged ( chbUseMIDIController->isChecked() ); + + QDialog::showEvent ( event ); +} + +bool CClientSettingsDlg::eventFilter ( QObject* obj, QEvent* event ) +{ + // Refresh MIDI device list when user clicks on the dropdown + if ( obj == cbxMidiDevice ) + { + if ( event->type() == QEvent::MouseButtonPress ) + { + // Refresh the device list without showing warnings (user is just browsing) + UpdateMIDIDeviceSelection ( false ); + } + } + + return QDialog::eventFilter ( obj, event ); } void CClientSettingsDlg::UpdateJitterBufferFrame() @@ -1216,3 +1457,194 @@ void CClientSettingsDlg::OnAudioPanValueChanged ( int value ) pClient->SetAudioInFader ( value ); UpdateAudioFaderSlider(); } + +void CClientSettingsDlg::ResetMidiLearn() +{ + midiLearnTarget = None; + + // Groupboxes corresponding to each learn button + QGroupBox* groupBoxes[5] = { grbMidiMuteMyself, grbMidiFader, grbMidiPan, grbMidiSolo, grbMidiMute }; + + for ( int i = 0; i < 5; i++ ) + { + midiLearnButtons[i]->setText ( tr ( "Learn" ) ); + // Only enable learn button if the corresponding groupbox is checked + midiLearnButtons[i]->setEnabled ( groupBoxes[i]->isChecked() ); + } +} + +void CClientSettingsDlg::SetMIDIControlsEnabled ( bool enabled ) +{ + midiControlsContainer->setEnabled ( enabled ); + // Enable/disable MIDI device combo box and label based on checkbox state + lblMidiDevice->setEnabled ( enabled ); + cbxMidiDevice->setEnabled ( enabled ); +} + +void CClientSettingsDlg::UpdateMIDIDeviceSelection ( bool bShowWarnings ) +{ + // Clear and repopulate the combo box + cbxMidiDevice->blockSignals ( true ); + cbxMidiDevice->clear(); + + // Populate device list (works on all platforms) + QStringList deviceNames = pClient->GetMIDIDevNames(); + +#if defined( _WIN32 ) && !defined( WITH_JACK ) + // Add "All Devices" as first option (Windows only) + cbxMidiDevice->addItem ( tr ( "All Devices" ), QString ( "" ) ); +#else + // Add "No device connected" as first option (Linux/macOS) + cbxMidiDevice->addItem ( tr ( "No device connected" ), QString ( "" ) ); +#endif + + // Add individual device names + for ( const QString& deviceName : deviceNames ) + { + cbxMidiDevice->addItem ( deviceName, deviceName ); + } + + // Set current selection based on settings + QString currentDevice = pSettings->strMidiDevice; + int iCurDevIdx = 0; // Default to first item (All Devices or No device connected) + + if ( !currentDevice.isEmpty() ) + { + iCurDevIdx = cbxMidiDevice->findData ( currentDevice ); + if ( iCurDevIdx < 0 ) + { + // Device not found - fall back to "No device connected" + iCurDevIdx = 0; + + // Only show warning at launch if MIDI is enabled and user should know + if ( bShowWarnings && pSettings->bUseMIDIController && pClient->IsMIDIEnabled() ) + { +#if defined( _WIN32 ) && !defined( WITH_JACK ) + // On Windows, first item is "All Devices" + QMessageBox::warning ( + this, + tr ( "MIDI Device Not Found" ), + tr ( "The MIDI device \"%1\" could not be found. Using all available devices instead." ).arg ( currentDevice ) ); +#else + // On Linux/macOS, Jamulus will auto-connect to selected device + QMessageBox::information ( + this, + tr ( "MIDI Device Not Found" ), + tr ( "The MIDI device \"%1\" is not currently available. Select a different device from the dropdown to connect." ) + .arg ( currentDevice ) ); +#endif + } + + // Clear saved device since it's not available + pSettings->strMidiDevice = ""; + } + } + + cbxMidiDevice->setCurrentIndex ( iCurDevIdx ); + cbxMidiDevice->setEnabled ( true ); + wdgMidiDeviceContainer->setVisible ( true ); + + cbxMidiDevice->blockSignals ( false ); +} + +void CClientSettingsDlg::OnMidiDeviceActivated ( int iMidiDevIdx ) +{ + if ( iMidiDevIdx < 0 || iMidiDevIdx >= cbxMidiDevice->count() ) + { + return; + } + + // Get the device name from combo box data + QString selectedDevice = cbxMidiDevice->itemData ( iMidiDevIdx ).toString(); + + // Update settings + pSettings->strMidiDevice = selectedDevice; + + // Changing device requires restarting MIDI to reconnect + if ( pSettings->bUseMIDIController && pClient->IsMIDIEnabled() ) + { + // Disable MIDI + pClient->EnableMIDI ( false ); + + // Set the new device + pClient->SetMIDIDevice ( selectedDevice ); + + // Re-enable MIDI + pClient->EnableMIDI ( true ); + + // Check if re-enable was successful + if ( !pClient->IsMIDIEnabled() ) + { +#if defined( _WIN32 ) && !defined( WITH_JACK ) + QMessageBox::warning ( this, + tr ( "MIDI Device Connection Failed" ), + tr ( "Could not connect to MIDI device \"%1\". Please check your OS configuration." ) + .arg ( selectedDevice.isEmpty() ? tr ( "All Devices" ) : selectedDevice ) ); +#else + QMessageBox::warning ( + this, + tr ( "MIDI Device Connection Failed" ), + tr ( "Could not connect to MIDI device \"%1\". Please check that the device is available." ).arg ( selectedDevice ) ); +#endif + } + } + else + { + // Just update the device setting for next time MIDI is enabled + pClient->SetMIDIDevice ( selectedDevice ); + } +} + +void CClientSettingsDlg::SetMidiLearnTarget ( MidiLearnTarget target, QPushButton* activeButton ) +{ + if ( midiLearnTarget == target ) + { + ResetMidiLearn(); + return; + } + + ResetMidiLearn(); + midiLearnTarget = target; + activeButton->setText ( tr ( "Listening..." ) ); + + // Disable all buttons except the active one + for ( QPushButton* button : midiLearnButtons ) + { + button->setEnabled ( button == activeButton ); + } +} + +void CClientSettingsDlg::OnLearnButtonClicked() +{ + QPushButton* sender = qobject_cast ( QObject::sender() ); + static const QMap buttonToTarget = { { butLearnMuteMyself, MuteMyself }, + { butLearnFaderOffset, Fader }, + { butLearnPanOffset, Pan }, + { butLearnSoloOffset, Solo }, + { butLearnMuteOffset, Mute } }; + SetMidiLearnTarget ( buttonToTarget.value ( sender, None ), sender ); +} + +void CClientSettingsDlg::OnMidiCCReceived ( int ccNumber ) +{ + if ( midiLearnTarget == None ) + return; + + // Validate MIDI CC number is within valid range (0-127) + if ( ccNumber < 0 || ccNumber > 127 ) + { + qWarning() << "CClientSettingsDlg::OnMidiCCReceived: Invalid MIDI CC number received:" << ccNumber; + return; + } + + static const QMap midiTargetToSpinBox = { { Fader, spnFaderOffset }, + { Pan, spnPanOffset }, + { Solo, spnSoloOffset }, + { Mute, spnMuteOffset }, + { MuteMyself, spnMuteMyself } }; + + if ( midiTargetToSpinBox.contains ( midiLearnTarget ) ) + midiTargetToSpinBox.value ( midiLearnTarget )->setValue ( ccNumber ); + + ResetMidiLearn(); +} diff --git a/src/clientsettingsdlg.h b/src/clientsettingsdlg.h index 7512e70236..1195ca014a 100644 --- a/src/clientsettingsdlg.h +++ b/src/clientsettingsdlg.h @@ -70,6 +70,7 @@ class CClientSettingsDlg : public CBaseDlg, private Ui_CClientSettingsDlgBase QString GenSndCrdBufferDelayString ( const int iFrameSize, const QString strAddText = "" ); virtual void showEvent ( QShowEvent* ); + virtual bool eventFilter ( QObject* obj, QEvent* event ) override; CClient* pClient; CClientSettings* pSettings; @@ -106,6 +107,7 @@ public slots: void OnTabChanged(); void OnMakeTabChange ( int iTabIdx ); void OnAudioPanValueChanged ( int value ); + void OnMidiDeviceActivated ( int iMidiDevIdx ); #if defined( _WIN32 ) && !defined( WITH_JACK ) // Only include this slot for Windows when JACK is NOT used @@ -119,4 +121,27 @@ public slots: void AudioChannelsChanged(); void CustomDirectoriesChanged(); void NumMixerPanelRowsChanged ( int value ); + void MIDIControllerUsageChanged ( bool bEnabled ); + +private: + enum MidiLearnTarget + { + None, + MuteMyself, + Fader, + Pan, + Solo, + Mute + }; + MidiLearnTarget midiLearnTarget; + + QPushButton* midiLearnButtons[5]; + void SetMidiLearnTarget ( MidiLearnTarget target, QPushButton* activeButton ); + void ResetMidiLearn(); + void SetMIDIControlsEnabled ( bool enabled ); + void UpdateMIDIDeviceSelection ( bool bShowWarnings = true ); + +private slots: + void OnLearnButtonClicked(); + void OnMidiCCReceived ( int ccNumber ); }; diff --git a/src/clientsettingsdlgbase.ui b/src/clientsettingsdlgbase.ui index 7ee311701a..bc6975a44f 100644 --- a/src/clientsettingsdlgbase.ui +++ b/src/clientsettingsdlgbase.ui @@ -6,8 +6,8 @@ 0 0 - 436 - 524 + 677 + 607 @@ -24,13 +24,13 @@ - + 0 0 - 1 + 3 true @@ -1336,6 +1336,859 @@ + + + MIDI Control + + + + + + + 0 + 0 + + + + MIDI-in + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + MIDI Device + + + + + + + + 350 + 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + + + + + 200 + 16777215 + + + + + 50 + false + + + + MIDI Channel + + + + + + + + 80 + 0 + + + + + 80 + 16777215 + + + + + 50 + false + + + + + 0 (all) + + + + + 1 + + + + + 2 + + + + + 3 + + + + + 4 + + + + + 5 + + + + + 6 + + + + + 7 + + + + + 8 + + + + + 9 + + + + + 10 + + + + + 11 + + + + + 12 + + + + + 13 + + + + + 14 + + + + + 15 + + + + + 16 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 75 + true + + + + Mute Myself + + + true + + + + + + + + + 103 + 0 + + + + + 50 + false + + + + MIDI CC + + + + + + + + 75 + 0 + + + + + 50 + false + + + + 0 + + + 127 + + + + + + + + 50 + false + + + + Learn + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 50 + false + + + + Fader + + + true + + + + + + + + + 50 + false + + + + First MIDI CC + + + + + + + + 75 + 0 + + + + + 50 + false + + + + 127 + + + + + + + + 50 + false + + + + Learn + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 50 + false + + + + Count + + + + + + + + 75 + 0 + + + + + 50 + false + + + + 1 + + + 127 + + + + + + + + + + + + + 50 + false + + + + Mute + + + true + + + + + + + + + 50 + false + + + + First MIDI CC + + + + + + + + 75 + 0 + + + + + 50 + false + + + + 127 + + + + + + + + 50 + false + + + + Learn + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 50 + false + + + + Count + + + + + + + + 75 + 0 + + + + + 50 + false + + + + 1 + + + 127 + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 50 + false + + + + Solo + + + true + + + + + + + + + 50 + false + + + + First MIDI CC + + + + + + + + 75 + 0 + + + + + 50 + false + + + + 127 + + + + + + + + 50 + false + + + + Learn + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 50 + false + + + + Count + + + + + + + + 75 + 0 + + + + + 50 + false + + + + 1 + + + 127 + + + + + + + + + + + + + 50 + false + + + + Pan + + + true + + + + + + + + + 50 + false + + + + First MIDI CC + + + + + + + + 75 + 0 + + + + + 50 + false + + + + 127 + + + + + + + + 50 + false + + + + Learn + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 50 + false + + + + Count + + + + + + + + 75 + 0 + + + + + 50 + false + + + + 1 + + + 127 + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + @@ -1348,6 +2201,7 @@ + tabSettings pedtAlias pcbxInstrument pcbxCountry @@ -1379,6 +2233,28 @@ cbxInputBoost chbDetectFeedback sldAudioPan + chbUseMIDIController + cbxMidiDevice + cbxChannel + grbMidiMuteMyself + spnMuteMyself + butLearnMuteMyself + grbMidiFader + spnFaderOffset + butLearnFaderOffset + spnFaderCount + grbMidiPan + spnPanOffset + butLearnPanOffset + spnPanCount + grbMidiSolo + spnSoloOffset + butLearnSoloOffset + spnSoloCount + grbMidiMute + spnMuteOffset + butLearnMuteOffset + spnMuteCount diff --git a/src/global.h b/src/global.h index 872d09f37d..46063c84af 100644 --- a/src/global.h +++ b/src/global.h @@ -267,6 +267,7 @@ LED bar: lbr #define SETTING_TAB_USER 0 #define SETTING_TAB_AUDIONET 1 #define SETTING_TAB_ADVANCED 2 +#define SETTING_TAB_MIDI 3 // common tool tip bottom line text #define TOOLTIP_COM_END_TEXT \ diff --git a/src/main.cpp b/src/main.cpp index 8e36f80e23..7739aa1f30 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -102,7 +102,6 @@ int main ( int argc, char** argv ) QString strJsonRpcBindIP = DEFAULT_JSON_RPC_LISTEN_ADDRESS; quint16 iQosNumber = DEFAULT_QOS_NUMBER; ELicenceType eLicenceType = LT_NO_LICENCE; - QString strMIDISetup = ""; QString strConnOnStartupAddress = ""; QString strIniFileName = ""; QString strHTMLStatusFileName = ""; @@ -535,7 +534,7 @@ int main ( int argc, char** argv ) continue; } - // Controller MIDI channel --------------------------------------------- + // MIDI if ( GetStringArgument ( argc, argv, i, @@ -543,9 +542,7 @@ int main ( int argc, char** argv ) "--ctrlmidich", strArgument ) ) { - strMIDISetup = strArgument; - qInfo() << qUtf8Printable ( QString ( "- MIDI controller settings: %1" ).arg ( strMIDISetup ) ); - CommandLineOptions << "--ctrlmidich"; + CommandLineOptions << QString ( "--ctrlmidich=%1" ).arg ( strArgument ); ClientOnlyOptions << "--ctrlmidich"; continue; } @@ -920,25 +917,17 @@ int main ( int argc, char** argv ) #ifndef SERVER_ONLY if ( bIsClient ) { - // Client: - // actual client object - CClient Client ( iPortNumber, - iQosNumber, - strConnOnStartupAddress, - strMIDISetup, - bNoAutoJackConnect, - strClientName, - bEnableIPv6, - bMuteMeInPersonalMix ); + CClient Client ( iPortNumber, iQosNumber, strConnOnStartupAddress, bNoAutoJackConnect, strClientName, bEnableIPv6, bMuteMeInPersonalMix ); - // load settings from init-file (command line options override) + // Create Settings with the client pointer CClientSettings Settings ( &Client, strIniFileName ); Settings.Load ( CommandLineOptions ); + Client.SetSettings ( &Settings ); # ifndef NO_JSON_RPC if ( pRpcServer ) { - new CClientRpc ( &Client, pRpcServer, pRpcServer ); + new CClientRpc ( &Client, &Settings, pRpcServer, pRpcServer ); } # endif @@ -956,7 +945,6 @@ int main ( int argc, char** argv ) CClientDlg ClientDlg ( &Client, &Settings, strConnOnStartupAddress, - strMIDISetup, bShowComplRegConnList, bShowAnalyzerConsole, bMuteStream, diff --git a/src/settings.cpp b/src/settings.cpp index 35c1f409a9..64022aac78 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -201,6 +201,135 @@ void CSettings::PutIniSetting ( QDomDocument& xmlFile, const QString& sSection, } #ifndef SERVER_ONLY + +// Parse MIDI commmand line parameters and update MIDI variables +void CClientSettings::ParseCtrlMidiCh ( const QString& strMidiMap, + int& iMidiChannel, + int& iMidiFaderOffset, + int& iMidiFaderCount, + int& iMidiPanOffset, + int& iMidiPanCount, + int& iMidiSoloOffset, + int& iMidiSoloCount, + int& iMidiMuteOffset, + int& iMidiMuteCount, + int& iMidiMuteMyself, + bool& bMidiFaderEnabled, + bool& bMidiPanEnabled, + bool& bMidiSoloEnabled, + bool& bMidiMuteEnabled, + bool& bMidiMuteMyselfEnabled, + bool& bUseMIDIController, + QString* strMIDIDevice ) +{ + if ( strMidiMap.isEmpty() ) + { + return; + } + + QStringList parts = strMidiMap.split ( ';' ); + if ( parts.isEmpty() ) + { + return; + } + + // Parse MIDI channel (first parameter) + iMidiChannel = parts[0].toInt(); + + // Check for legacy format: [channel];[offset] + // If second parameter is a plain number (no prefix), treat as legacy format + if ( parts.size() >= 2 ) + { + bool bIsNumber = false; + QString sParm = parts[1].trimmed(); + int iOffset = sParm.toInt ( &bIsNumber ); + + if ( bIsNumber && !sParm.isEmpty() ) + { + // Legacy format: set up faders from offset to 127 or MAX_NUM_CHANNELS + iMidiFaderOffset = iOffset; + iMidiFaderCount = qMin ( MAX_NUM_CHANNELS, 128 - iOffset ); + bUseMIDIController = true; + return; + } + } + + // Parse named controllers (new format) + for ( int i = 1; i < parts.size(); ++i ) + { + QString sParm = parts[i].trimmed(); + if ( sParm.isEmpty() ) + { + continue; + } + + QChar cType = sParm[0]; + + // Handle device selection + if ( cType == 'd' ) + { + if ( strMIDIDevice != nullptr ) + { + *strMIDIDevice = sParm.mid ( 1 ); + } + continue; + } + + // Parse controller specification: [type][offset]*[count] + // where [type] is f, p, s, m, or o + QStringList vals = sParm.mid ( 1 ).split ( '*' ); + int iFirst = vals[0].toInt(); + int iNum = ( vals.size() > 1 ) ? vals[1].toInt() : 1; + + // Bounds checking + if ( iFirst < 0 || iFirst >= 128 ) + { + continue; + } + + iNum = qMin ( iNum, MAX_NUM_CHANNELS ); + iNum = qMin ( iNum, 128 - iFirst ); + + if ( iNum <= 0 ) + { + continue; + } + + // Assign to appropriate controller type + if ( cType == 'f' ) + { + iMidiFaderOffset = iFirst; + iMidiFaderCount = iNum; + bMidiFaderEnabled = true; + } + else if ( cType == 'p' ) + { + iMidiPanOffset = iFirst; + iMidiPanCount = iNum; + bMidiPanEnabled = true; + } + else if ( cType == 's' ) + { + iMidiSoloOffset = iFirst; + iMidiSoloCount = iNum; + bMidiSoloEnabled = true; + } + else if ( cType == 'm' ) + { + iMidiMuteOffset = iFirst; + iMidiMuteCount = iNum; + bMidiMuteEnabled = true; + } + else if ( cType == 'o' ) + { + iMidiMuteMyself = iFirst; + bMidiMuteMyselfEnabled = true; + } + } + + bUseMIDIController = true; +} + // Client settings ------------------------------------------------------------- void CClientSettings::LoadFaderSettings ( const QString& strCurFileName ) { @@ -226,7 +355,7 @@ void CClientSettings::SaveFaderSettings ( const QString& strCurFileName ) WriteToFile ( strCurFileName, IniXMLDocument ); } -void CClientSettings::ReadSettingsFromXML ( const QDomDocument& IniXMLDocument, const QList& ) +void CClientSettings::ReadSettingsFromXML ( const QDomDocument& IniXMLDocument, const QList& CommandLineOptions ) { int iIdx; int iValue; @@ -461,6 +590,88 @@ void CClientSettings::ReadSettingsFromXML ( const QDomDocument& IniXMLDocument, pClient->SetAudioQuality ( static_cast ( iValue ) ); } + // MIDI settings: Always read from XML first to preserve values + if ( GetNumericIniSet ( IniXMLDocument, "client", "midichannel", 0, 16, iValue ) ) + iMidiChannel = iValue; + + struct MidiSettingEntry + { + const char* key; + int* variable; + }; + MidiSettingEntry midiSettings[] = { { "midifaderoffset", &iMidiFaderOffset }, + { "midifadercount", &iMidiFaderCount }, + { "midipanoffset", &iMidiPanOffset }, + { "midipancount", &iMidiPanCount }, + { "midisolooffset", &iMidiSoloOffset }, + { "midisolocount", &iMidiSoloCount }, + { "midimuteoffset", &iMidiMuteOffset }, + { "midimutecount", &iMidiMuteCount }, + { "midimutemyself", &iMidiMuteMyself } }; + for ( const auto& entry : midiSettings ) + { + if ( GetNumericIniSet ( IniXMLDocument, "client", entry.key, 0, 127, iValue ) ) + *( entry.variable ) = iValue; + } + if ( GetFlagIniSet ( IniXMLDocument, "client", "usemidicontroller", bValue ) ) + bUseMIDIController = bValue; + + // Read enable flags + if ( GetFlagIniSet ( IniXMLDocument, "client", "midifaderenabled", bValue ) ) + bMidiFaderEnabled = bValue; + if ( GetFlagIniSet ( IniXMLDocument, "client", "midipanenable", bValue ) ) + bMidiPanEnabled = bValue; + if ( GetFlagIniSet ( IniXMLDocument, "client", "midisoloenabled", bValue ) ) + bMidiSoloEnabled = bValue; + if ( GetFlagIniSet ( IniXMLDocument, "client", "midimuteenabled", bValue ) ) + bMidiMuteEnabled = bValue; + if ( GetFlagIniSet ( IniXMLDocument, "client", "midimutemyselfenabled", bValue ) ) + bMidiMuteMyselfEnabled = bValue; + + // Read MIDI device name from settings + strMidiDevice = GetIniSetting ( IniXMLDocument, "client", "mididevice_base64", "" ); + if ( !strMidiDevice.isEmpty() ) + { + strMidiDevice = FromBase64ToString ( strMidiDevice ); + } + + // Command line overrides: disable all controls, then re-enable only those specified + for ( const QString& option : CommandLineOptions ) + { + if ( option.startsWith ( "--ctrlmidich=" ) ) + { + QString strMidiMap = option.section ( '=', 1 ); + + // Disable all controls first - command line will only enable what's specified + bMidiFaderEnabled = false; + bMidiPanEnabled = false; + bMidiSoloEnabled = false; + bMidiMuteEnabled = false; + bMidiMuteMyselfEnabled = false; + + // Parse command line - this will re-enable specified controls and update their values + CClientSettings::ParseCtrlMidiCh ( strMidiMap, + iMidiChannel, + iMidiFaderOffset, + iMidiFaderCount, + iMidiPanOffset, + iMidiPanCount, + iMidiSoloOffset, + iMidiSoloCount, + iMidiMuteOffset, + iMidiMuteCount, + iMidiMuteMyself, + bMidiFaderEnabled, + bMidiPanEnabled, + bMidiSoloEnabled, + bMidiMuteEnabled, + bMidiMuteMyselfEnabled, + bUseMIDIController, + &strMidiDevice ); + break; + } + } + // custom directories //### TODO: BEGIN ###// @@ -548,7 +759,7 @@ void CClientSettings::ReadSettingsFromXML ( const QDomDocument& IniXMLDocument, } // selected Settings Tab - if ( GetNumericIniSet ( IniXMLDocument, "client", "settingstab", 0, 2, iValue ) ) + if ( GetNumericIniSet ( IniXMLDocument, "client", "settingstab", 0, 3, iValue ) ) { iSettingsTab = iValue; } @@ -556,7 +767,6 @@ void CClientSettings::ReadSettingsFromXML ( const QDomDocument& IniXMLDocument, // fader settings ReadFaderSettingsFromXML ( IniXMLDocument ); } - void CClientSettings::ReadFaderSettingsFromXML ( const QDomDocument& IniXMLDocument ) { int iIdx; @@ -754,6 +964,30 @@ void CClientSettings::WriteSettingsToXML ( QDomDocument& IniXMLDocument, bool is // Settings Tab SetNumericIniSet ( IniXMLDocument, "client", "settingstab", iSettingsTab ); + // MIDI settings + SetNumericIniSet ( IniXMLDocument, "client", "midichannel", iMidiChannel ); + SetNumericIniSet ( IniXMLDocument, "client", "midifaderoffset", iMidiFaderOffset ); + SetNumericIniSet ( IniXMLDocument, "client", "midifadercount", iMidiFaderCount ); + SetNumericIniSet ( IniXMLDocument, "client", "midipanoffset", iMidiPanOffset ); + SetNumericIniSet ( IniXMLDocument, "client", "midipancount", iMidiPanCount ); + SetNumericIniSet ( IniXMLDocument, "client", "midisolooffset", iMidiSoloOffset ); + SetNumericIniSet ( IniXMLDocument, "client", "midisolocount", iMidiSoloCount ); + SetNumericIniSet ( IniXMLDocument, "client", "midimuteoffset", iMidiMuteOffset ); + SetNumericIniSet ( IniXMLDocument, "client", "midimutecount", iMidiMuteCount ); + SetNumericIniSet ( IniXMLDocument, "client", "midimutemyself", iMidiMuteMyself ); + SetFlagIniSet ( IniXMLDocument, "client", "usemidicontroller", bUseMIDIController ); + SetFlagIniSet ( IniXMLDocument, "client", "midifaderenabled", bMidiFaderEnabled ); + SetFlagIniSet ( IniXMLDocument, "client", "midipanenable", bMidiPanEnabled ); + SetFlagIniSet ( IniXMLDocument, "client", "midisoloenabled", bMidiSoloEnabled ); + SetFlagIniSet ( IniXMLDocument, "client", "midimuteenabled", bMidiMuteEnabled ); + SetFlagIniSet ( IniXMLDocument, "client", "midimutemyselfenabled", bMidiMuteMyselfEnabled ); + + // Save MIDI device name + if ( !strMidiDevice.isEmpty() ) + { + PutIniSetting ( IniXMLDocument, "client", "mididevice_base64", ToBase64 ( strMidiDevice ) ); + } + // fader settings WriteFaderSettingsToXML ( IniXMLDocument ); } diff --git a/src/settings.h b/src/settings.h index d87854d992..a18ffb455d 100644 --- a/src/settings.h +++ b/src/settings.h @@ -164,6 +164,23 @@ class CClientSettings : public CSettings bWindowWasShownChat ( false ), bWindowWasShownConnect ( false ), bOwnFaderFirst ( false ), + iMidiChannel ( 0 ), + iMidiMuteMyself ( 0 ), + iMidiFaderOffset ( 0 ), + iMidiFaderCount ( 0 ), + iMidiPanOffset ( 0 ), + iMidiPanCount ( 0 ), + iMidiSoloOffset ( 0 ), + iMidiSoloCount ( 0 ), + iMidiMuteOffset ( 0 ), + iMidiMuteCount ( 0 ), + bMidiFaderEnabled ( false ), + bMidiPanEnabled ( false ), + bMidiSoloEnabled ( false ), + bMidiMuteEnabled ( false ), + bMidiMuteMyselfEnabled ( false ), + bUseMIDIController ( false ), + strMidiDevice ( "" ), pClient ( pNCliP ) { SetFileName ( sNFiName, DEFAULT_INI_FILE_NAME ); @@ -172,6 +189,26 @@ class CClientSettings : public CSettings void LoadFaderSettings ( const QString& strCurFileName ); void SaveFaderSettings ( const QString& strCurFileName ); + // Parse a --ctrlmidich MIDI mapping string and update MIDI variables + static void ParseCtrlMidiCh ( const QString& strMidiMap, + int& iMidiChannel, + int& iMidiFaderOffset, + int& iMidiFaderCount, + int& iMidiPanOffset, + int& iMidiPanCount, + int& iMidiSoloOffset, + int& iMidiSoloCount, + int& iMidiMuteOffset, + int& iMidiMuteCount, + int& iMidiMuteMyself, + bool& bMidiFaderEnabled, + bool& bMidiPanEnabled, + bool& bMidiSoloEnabled, + bool& bMidiMuteEnabled, + bool& bMidiMuteMyselfEnabled, + bool& bUseMIDIController, + QString* strMIDIDevice = nullptr ); + // general settings CVector vecStoredFaderTags; CVector vecStoredFaderLevels; @@ -201,6 +238,25 @@ class CClientSettings : public CSettings bool bWindowWasShownConnect; bool bOwnFaderFirst; + // MIDI settings + int iMidiChannel; + int iMidiMuteMyself; + int iMidiFaderOffset; + int iMidiFaderCount; + int iMidiPanOffset; + int iMidiPanCount; + int iMidiSoloOffset; + int iMidiSoloCount; + int iMidiMuteOffset; + int iMidiMuteCount; + bool bMidiFaderEnabled; + bool bMidiPanEnabled; + bool bMidiSoloEnabled; + bool bMidiMuteEnabled; + bool bMidiMuteMyselfEnabled; + bool bUseMIDIController; + QString strMidiDevice; + protected: virtual void WriteSettingsToXML ( QDomDocument& IniXMLDocument, bool isAboutToQuit ) override; virtual void ReadSettingsFromXML ( const QDomDocument& IniXMLDocument, const QList& ) override; diff --git a/src/sound/asio/sound.cpp b/src/sound/asio/sound.cpp index 40adacb1d2..4deb92b8ed 100644 --- a/src/sound/asio/sound.cpp +++ b/src/sound/asio/sound.cpp @@ -515,12 +515,8 @@ void CSound::Stop() } } -CSound::CSound ( void ( *fpNewCallback ) ( CVector& psData, void* arg ), - void* arg, - const QString& strMIDISetup, - const bool, - const QString& ) : - CSoundBase ( "ASIO", fpNewCallback, arg, strMIDISetup ), +CSound::CSound ( void ( *fpNewCallback ) ( CVector& psData, void* arg ), void* arg, const bool, const QString& ) : + CSoundBase ( "ASIO", fpNewCallback, arg ), lNumInChan ( 0 ), lNumInChanPlusAddChan ( 0 ), lNumOutChan ( 0 ), @@ -629,6 +625,48 @@ bool CSound::CheckSampleTypeSupportedForCHMixing ( const ASIOSampleType SamType return ( ( SamType == ASIOSTInt16LSB ) || ( SamType == ASIOSTInt24LSB ) || ( SamType == ASIOSTInt32LSB ) ); } +void CSound::EnableMIDI ( bool bEnable ) +{ + if ( bEnable ) + { + // Enable MIDI only if it's not already enabled + if ( !bMidiEnabled && iCtrlMIDIChannel != INVALID_MIDI_CH ) + { + Midi.MidiStart(); + } + } + else + { + // Disable MIDI only if it's currently enabled + if ( bMidiEnabled ) + { + Midi.MidiStop(); + } + } + bMidiEnabled = Midi.IsActive(); +} + +bool CSound::IsMIDIEnabled() const { return bMidiEnabled; } + +QStringList CSound::GetMIDIDevNames() +{ + QStringList deviceNamesList; + int numDevices = midiInGetNumDevs(); + + for ( int i = 0; i < numDevices; i++ ) + { + MIDIINCAPS mic; + MMRESULT result = midiInGetDevCaps ( i, &mic, sizeof ( MIDIINCAPS ) ); + + if ( result == MMSYSERR_NOERROR ) + { + deviceNamesList.append ( QString ( mic.szPname ) ); + } + } + + return deviceNamesList; +} + void CSound::bufferSwitch ( long index, ASIOBool ) { int iCurSample; diff --git a/src/sound/asio/sound.h b/src/sound/asio/sound.h index 3e11482c96..19ce0d10cb 100644 --- a/src/sound/asio/sound.h +++ b/src/sound/asio/sound.h @@ -55,7 +55,7 @@ class CSound : public CSoundBase Q_OBJECT public: - CSound ( void ( *fpNewCallback ) ( CVector& psData, void* arg ), void* arg, const QString& strMIDISetup, const bool, const QString& ); + CSound ( void ( *fpNewCallback ) ( CVector& psData, void* arg ), void* arg, const bool, const QString& ); virtual ~CSound(); @@ -82,6 +82,11 @@ class CSound : public CSoundBase virtual float GetInOutLatencyMs() { return fInOutLatencyMs; } + // MIDI port toggle + virtual void EnableMIDI ( bool bEnable ); + virtual bool IsMIDIEnabled() const; + virtual QStringList GetMIDIDevNames(); + protected: virtual QString LoadAndInitializeDriver ( QString strDriverName, bool bOpenDriverSetup ); virtual void UnloadCurrentDriver(); @@ -138,4 +143,7 @@ class CSound : public CSoundBase // Windows native MIDI support CMidi Midi; + +private: + bool bMidiEnabled = false; // Tracks the runtime state of MIDI }; diff --git a/src/sound/coreaudio-ios/sound.h b/src/sound/coreaudio-ios/sound.h index af78c6a74e..475f759c7d 100644 --- a/src/sound/coreaudio-ios/sound.h +++ b/src/sound/coreaudio-ios/sound.h @@ -33,11 +33,7 @@ class CSound : public CSoundBase Q_OBJECT public: - CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), - void* arg, - const QString& strMIDISetup, - const bool, - const QString& ); + CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), void* arg, const bool, const QString& ); ~CSound(); virtual int Init ( const int iNewPrefMonoBufferSize ); diff --git a/src/sound/coreaudio-ios/sound.mm b/src/sound/coreaudio-ios/sound.mm index 807272b02e..33f47ee630 100644 --- a/src/sound/coreaudio-ios/sound.mm +++ b/src/sound/coreaudio-ios/sound.mm @@ -28,12 +28,8 @@ #define kInputBus 1 /* Implementation *************************************************************/ -CSound::CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), - void* arg, - const QString& strMIDISetup, - const bool, - const QString& ) : - CSoundBase ( "CoreAudio iOS", fpNewProcessCallback, arg, strMIDISetup ), +CSound::CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), void* arg, const bool, const QString& ) : + CSoundBase ( "CoreAudio iOS", fpNewProcessCallback, arg ), isInitialized ( false ) { try diff --git a/src/sound/coreaudio-mac/sound.cpp b/src/sound/coreaudio-mac/sound.cpp index 51e8775f67..76b826b6c1 100644 --- a/src/sound/coreaudio-mac/sound.cpp +++ b/src/sound/coreaudio-mac/sound.cpp @@ -25,12 +25,9 @@ #include "sound.h" /* Implementation *************************************************************/ -CSound::CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), - void* arg, - const QString& strMIDISetup, - const bool, - const QString& ) : - CSoundBase ( "CoreAudio", fpNewProcessCallback, arg, strMIDISetup ), +CSound::CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), void* arg, const bool, const QString& ) : + CSoundBase ( "CoreAudio", fpNewProcessCallback, arg ), + midiClient ( static_cast ( NULL ) ), midiInPortRef ( static_cast ( NULL ) ) { // Apple Mailing Lists: Subject: GUI Apps should set kAudioHardwarePropertyRunLoop @@ -60,23 +57,22 @@ CSound::CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* iSelInputRightChannel = 0; iSelOutputLeftChannel = 0; iSelOutputRightChannel = 0; +} - // Optional MIDI initialization -------------------------------------------- - if ( iCtrlMIDIChannel != INVALID_MIDI_CH ) - { - // create client and ports - MIDIClientRef midiClient = static_cast ( NULL ); - MIDIClientCreate ( CFSTR ( APP_NAME ), NULL, NULL, &midiClient ); - MIDIInputPortCreate ( midiClient, CFSTR ( "Input port" ), callbackMIDI, this, &midiInPortRef ); - - // open connections from all sources - const int iNMIDISources = MIDIGetNumberOfSources(); +CSound::~CSound() +{ + // Ensure MIDI resources are properly cleaned up + DestroyMIDIPort(); // This will destroy the port if it exists - for ( int i = 0; i < iNMIDISources; i++ ) + // Explicitly destroy the client if it exists + if ( midiClient != static_cast ( NULL ) ) + { + OSStatus result = MIDIClientDispose ( midiClient ); + if ( result != noErr ) { - MIDIEndpointRef src = MIDIGetSource ( i ); - MIDIPortConnectSource ( midiInPortRef, src, NULL ); + qWarning() << "Failed to dispose CoreAudio MIDI client in destructor. Error code:" << result; } + midiClient = static_cast ( NULL ); } } @@ -718,6 +714,129 @@ void CSound::Stop() CSoundBase::Stop(); } +void CSound::EnableMIDI ( bool bEnable ) +{ + if ( bEnable && ( iCtrlMIDIChannel != INVALID_MIDI_CH ) ) + { + // Create MIDI port if we have valid MIDI channel and no port exists + if ( midiInPortRef == static_cast ( NULL ) ) + { + CreateMIDIPort(); + } + } + else + { + // Destroy MIDI port if it exists + if ( midiInPortRef != static_cast ( NULL ) ) + { + DestroyMIDIPort(); + } + } +} + +bool CSound::IsMIDIEnabled() const { return ( midiInPortRef != static_cast ( NULL ) ); } + +QStringList CSound::GetMIDIDevNames() +{ + QStringList deviceNamesList; + + // Get all available MIDI sources + const int iNMIDISources = MIDIGetNumberOfSources(); + + for ( int i = 0; i < iNMIDISources; i++ ) + { + MIDIEndpointRef src = MIDIGetSource ( i ); + CFStringRef deviceName; + + // Get the name of the MIDI source + OSStatus result = MIDIObjectGetStringProperty ( src, kMIDIPropertyDisplayName, &deviceName ); + if ( result == noErr && deviceName != nullptr ) + { + QString name = QString::fromCFString ( deviceName ); + deviceNamesList.append ( name ); + CFRelease ( deviceName ); + } + else + { + // Fallback to generic name if display name not available + deviceNamesList.append ( QString ( "MIDI Source %1" ).arg ( i ) ); + } + } + + return deviceNamesList; +} + +void CSound::CreateMIDIPort() +{ + if ( midiClient == static_cast ( NULL ) ) + { + // Create MIDI client + OSStatus result = MIDIClientCreate ( CFSTR ( APP_NAME ), NULL, NULL, &midiClient ); + if ( result != noErr ) + { + qWarning() << "Failed to create CoreAudio MIDI client. Error code:" << result; + return; + } + } + + if ( midiInPortRef == static_cast ( NULL ) ) + { + // Create MIDI input port + OSStatus result = MIDIInputPortCreate ( midiClient, CFSTR ( "Input port" ), callbackMIDI, this, &midiInPortRef ); + if ( result != noErr ) + { + qWarning() << "Failed to create CoreAudio MIDI input port. Error code:" << result; + return; + } + + // Connect to selected MIDI device if one is specified + if ( !strMIDIDevice.isEmpty() ) + { + // Find the MIDI source by name + const int iNMIDISources = MIDIGetNumberOfSources(); + for ( int i = 0; i < iNMIDISources; i++ ) + { + MIDIEndpointRef src = MIDIGetSource ( i ); + CFStringRef deviceName; + + OSStatus nameResult = MIDIObjectGetStringProperty ( src, kMIDIPropertyDisplayName, &deviceName ); + if ( nameResult == noErr && deviceName != nullptr ) + { + QString name = QString::fromCFString ( deviceName ); + CFRelease ( deviceName ); + + if ( name == strMIDIDevice ) + { + // Connect to this source + result = MIDIPortConnectSource ( midiInPortRef, src, nullptr ); + if ( result != noErr ) + { + qWarning() << "Failed to connect to MIDI source" << strMIDIDevice << ". Error code:" << result; + } + break; + } + } + } + } + } +} + +void CSound::DestroyMIDIPort() +{ + if ( midiInPortRef != static_cast ( NULL ) ) + { + // Dispose of the MIDI input port (connections are automatically cleaned up) + OSStatus result = MIDIPortDispose ( midiInPortRef ); + if ( result != noErr ) + { + qWarning() << "Failed to dispose CoreAudio MIDI input port. Error code:" << result; + } + midiInPortRef = static_cast ( NULL ); + + qInfo() << "CoreAudio MIDI port destroyed"; + } +} + int CSound::Init ( const int iNewPrefMonoBufferSize ) { UInt32 iActualMonoBufferSize; diff --git a/src/sound/coreaudio-mac/sound.h b/src/sound/coreaudio-mac/sound.h index 6f1a0a1950..6ed3990a19 100644 --- a/src/sound/coreaudio-mac/sound.h +++ b/src/sound/coreaudio-mac/sound.h @@ -38,11 +38,9 @@ class CSound : public CSoundBase Q_OBJECT public: - CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), - void* arg, - const QString& strMIDISetup, - const bool, - const QString& ); + CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), void* arg, const bool, const QString& ); + + virtual ~CSound(); virtual int Init ( const int iNewPrefMonoBufferSize ); virtual void Start(); @@ -63,6 +61,11 @@ class CSound : public CSoundBase virtual int GetLeftOutputChannel() { return iSelOutputLeftChannel; } virtual int GetRightOutputChannel() { return iSelOutputRightChannel; } + // MIDI functions + virtual void EnableMIDI ( const bool bEnable ); + virtual bool IsMIDIEnabled() const; + virtual QStringList GetMIDIDevNames() override; + // these variables/functions should be protected but cannot since we want // to access them from the callback function CVector vecsTmpAudioSndCrdStereo; @@ -108,6 +111,9 @@ class CSound : public CSoundBase bool ConvertCFStringToQString ( const CFStringRef stringRef, QString& sOut ); + void CreateMIDIPort(); + void DestroyMIDIPort(); + // callbacks static OSStatus deviceNotification ( AudioDeviceID, UInt32, const AudioObjectPropertyAddress* inAddresses, void* inRefCon ); @@ -126,7 +132,8 @@ class CSound : public CSoundBase AudioDeviceIOProcID audioInputProcID; AudioDeviceIOProcID audioOutputProcID; - MIDIPortRef midiInPortRef; + MIDIClientRef midiClient; + MIDIPortRef midiInPortRef; QString sChannelNamesInput[MAX_NUM_IN_OUT_CHANNELS]; QString sChannelNamesOutput[MAX_NUM_IN_OUT_CHANNELS]; diff --git a/src/sound/jack/sound.cpp b/src/sound/jack/sound.cpp index 1b272923c9..109a5313ec 100644 --- a/src/sound/jack/sound.cpp +++ b/src/sound/jack/sound.cpp @@ -85,21 +85,7 @@ void CSound::OpenJack ( const bool bNoAutoJackConnect, const char* jackClientNam } // optional MIDI initialization - if ( iCtrlMIDIChannel != INVALID_MIDI_CH ) - { - input_port_midi = jack_port_register ( pJackClient, "input midi", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0 ); - - if ( input_port_midi == nullptr ) - { - throw CGenErr ( QString ( tr ( "The JACK port registration failed. This is probably an error with JACK. Please stop %1 and JACK. " - "Afterwards, check if another MIDI program can connect to JACK." ) ) - .arg ( APP_NAME ) ); - } - } - else - { - input_port_midi = nullptr; - } + input_port_midi = nullptr; // tell the JACK server that we are ready to roll if ( jack_activate ( pJackClient ) ) @@ -192,6 +178,92 @@ void CSound::Stop() CSoundBase::Stop(); } +void CSound::EnableMIDI ( bool bEnable ) +{ + if ( bEnable && ( iCtrlMIDIChannel != INVALID_MIDI_CH ) ) + { + // Create MIDI port if we have a valid MIDI channel and no port exists + if ( input_port_midi == nullptr ) + { + CreateMIDIPort(); + } + } + else + { + // Destroy MIDI port if it exists + if ( input_port_midi != nullptr ) + { + DestroyMIDIPort(); + } + } +} + +bool CSound::IsMIDIEnabled() const { return ( input_port_midi != nullptr ); } + +QStringList CSound::GetMIDIDevNames() +{ + QStringList deviceNamesList; + + if ( pJackClient == nullptr ) + { + return deviceNamesList; + } + + // Get all MIDI output ports (which are inputs from Jamulus' perspective) + const char** ports = jack_get_ports ( pJackClient, nullptr, JACK_DEFAULT_MIDI_TYPE, JackPortIsOutput ); + + if ( ports != nullptr ) + { + for ( int i = 0; ports[i] != nullptr; i++ ) + { + deviceNamesList.append ( QString ( ports[i] ) ); + } + + jack_free ( ports ); + } + + return deviceNamesList; +} + +void CSound::CreateMIDIPort() +{ + if ( pJackClient != nullptr && input_port_midi == nullptr ) + { + input_port_midi = jack_port_register ( pJackClient, "input midi", JACK_DEFAULT_MIDI_TYPE, JackPortIsInput, 0 ); + + if ( input_port_midi == nullptr ) + { + qWarning() << "Failed to create JACK MIDI port at runtime"; + return; + } + + // Connect to selected MIDI device if one is specified + if ( !strMIDIDevice.isEmpty() ) + { + const char* ourPortName = jack_port_name ( input_port_midi ); + if ( jack_connect ( pJackClient, strMIDIDevice.toUtf8().constData(), ourPortName ) != 0 ) + { + qWarning() << "Failed to connect JACK MIDI port" << strMIDIDevice << "to" << ourPortName; + } + } + } +} + +void CSound::DestroyMIDIPort() +{ + if ( pJackClient != nullptr && input_port_midi != nullptr ) + { + if ( jack_port_unregister ( pJackClient, input_port_midi ) == 0 ) + { + input_port_midi = nullptr; + } + else + { + qWarning() << "Failed to destroy JACK MIDI port"; + } + } +} + int CSound::Init ( const int /* iNewPrefMonoBufferSize */ ) { diff --git a/src/sound/jack/sound.h b/src/sound/jack/sound.h index aada015568..3c44d38f46 100644 --- a/src/sound/jack/sound.h +++ b/src/sound/jack/sound.h @@ -62,10 +62,9 @@ class CSound : public CSoundBase public: CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), void* arg, - const QString& strMIDISetup, const bool bNoAutoJackConnect, const QString& strJackClientName ) : - CSoundBase ( "Jack", fpNewProcessCallback, arg, strMIDISetup ), + CSoundBase ( "Jack", fpNewProcessCallback, arg ), iJACKBufferSizeMono ( 0 ), bJackWasShutDown ( false ), fInOutLatencyMs ( 0.0f ) @@ -86,7 +85,10 @@ class CSound : public CSoundBase virtual void Start(); virtual void Stop(); - virtual float GetInOutLatencyMs() { return fInOutLatencyMs; } + virtual float GetInOutLatencyMs() { return fInOutLatencyMs; } + virtual void EnableMIDI ( bool bEnable ) override; + virtual bool IsMIDIEnabled() const override; + virtual QStringList GetMIDIDevNames() override; // these variables should be protected but cannot since we want // to access them from the callback function @@ -105,6 +107,8 @@ class CSound : public CSoundBase void OpenJack ( const bool bNoAutoJackConnect, const char* jackClientName ); void CloseJack(); + void CreateMIDIPort(); + void DestroyMIDIPort(); // callbacks static int process ( jack_nframes_t nframes, void* arg ); @@ -121,12 +125,8 @@ class CSound : public CSoundBase Q_OBJECT public: - CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* pParg ), - void* pParg, - const QString& strMIDISetup, - const bool, - const QString& ) : - CSoundBase ( "nosound", fpNewProcessCallback, pParg, strMIDISetup ), + CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* pParg ), void* pParg, const bool, const QString& ) : + CSoundBase ( "nosound", fpNewProcessCallback, pParg ), HighPrecisionTimer ( true ) { HighPrecisionTimer.Start(); diff --git a/src/sound/midi-win/midi.cpp b/src/sound/midi-win/midi.cpp index 6823f6b6f3..1501e21ade 100644 --- a/src/sound/midi-win/midi.cpp +++ b/src/sound/midi-win/midi.cpp @@ -39,6 +39,11 @@ extern CSound* pSound; void CMidi::MidiStart() { + if ( m_bIsActive ) + { + return; // MIDI is already active, no need to start again + } + QString selMIDIDevice = pSound->GetMIDIDevice(); /* Get the number of MIDI In devices in this computer */ @@ -87,21 +92,36 @@ void CMidi::MidiStart() continue; // try next device, if any } - // success, add it to list of open handles + // Success, add it to the list of open handles vecMidiInHandles.append ( hMidiIn ); } + + if ( !vecMidiInHandles.isEmpty() ) + { + m_bIsActive = true; // Set active state if at least one device was started + } } void CMidi::MidiStop() { - // stop MIDI if running + if ( !m_bIsActive ) + { + return; // MIDI is already stopped, no need to stop again + } + + // Stop MIDI if running for ( int i = 0; i < vecMidiInHandles.size(); i++ ) { midiInStop ( vecMidiInHandles.at ( i ) ); midiInClose ( vecMidiInHandles.at ( i ) ); } + + vecMidiInHandles.clear(); // Clear the list of handles + m_bIsActive = false; // Set active state to false } +bool CMidi::IsActive() const { return m_bIsActive; } + // See https://learn.microsoft.com/en-us/previous-versions//dd798460(v=vs.85) // for the definition of the MIDI input callback function. void CALLBACK CMidi::MidiCallback ( HMIDIIN hMidiIn, UINT wMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2 ) diff --git a/src/sound/midi-win/midi.h b/src/sound/midi-win/midi.h index baa2f1286e..08154e0ab0 100644 --- a/src/sound/midi-win/midi.h +++ b/src/sound/midi-win/midi.h @@ -31,16 +31,18 @@ class CMidi { public: - CMidi() {} + CMidi() : m_bIsActive ( false ) {} virtual ~CMidi() {} void MidiStart(); void MidiStop(); + bool IsActive() const; protected: int iMidiDevs; QVector vecMidiInHandles; // windows handles + bool m_bIsActive; // Tracks if MIDI is currently active static void CALLBACK MidiCallback ( HMIDIIN hMidiIn, UINT wMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2 ); }; diff --git a/src/sound/oboe/sound.cpp b/src/sound/oboe/sound.cpp index 6886430bf2..c9e127077f 100644 --- a/src/sound/oboe/sound.cpp +++ b/src/sound/oboe/sound.cpp @@ -29,12 +29,8 @@ const uint8_t CSound::RING_FACTOR = 20; -CSound::CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), - void* arg, - const QString& strMIDISetup, - const bool, - const QString& ) : - CSoundBase ( "Oboe", fpNewProcessCallback, arg, strMIDISetup ) +CSound::CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), void* arg, const bool, const QString& ) : + CSoundBase ( "Oboe", fpNewProcessCallback, arg ) { #ifdef ANDROIDDEBUG qInstallMessageHandler ( myMessageHandler ); diff --git a/src/sound/oboe/sound.h b/src/sound/oboe/sound.h index e5b16c2daa..2c78fab08f 100644 --- a/src/sound/oboe/sound.h +++ b/src/sound/oboe/sound.h @@ -39,11 +39,7 @@ class CSound : public CSoundBase, public oboe::AudioStreamCallback public: static const uint8_t RING_FACTOR; - CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), - void* arg, - const QString& strMIDISetup, - const bool, - const QString& ); + CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), void* arg, const bool, const QString& ); virtual ~CSound() {} virtual int Init ( const int iNewPrefMonoBufferSize ); diff --git a/src/sound/soundbase.cpp b/src/sound/soundbase.cpp index a1e077e076..12043b510e 100644 --- a/src/sound/soundbase.cpp +++ b/src/sound/soundbase.cpp @@ -39,8 +39,7 @@ char const sMidiCtlChar[] = { /* Implementation *************************************************************/ CSoundBase::CSoundBase ( const QString& strNewSystemDriverTechniqueName, void ( *fpNewProcessCallback ) ( CVector& psData, void* pParg ), - void* pParg, - const QString& strMIDISetup ) : + void* pParg ) : fpProcessCallback ( fpNewProcessCallback ), pProcessCallbackArg ( pParg ), bRun ( false ), @@ -49,13 +48,9 @@ CSoundBase::CSoundBase ( const QString& strNewSystemDriverTechniqueName, iCtrlMIDIChannel ( INVALID_MIDI_CH ), aMidiCtls ( 128 ) { - // parse the MIDI setup command line argument string - ParseCommandLineArgument ( strMIDISetup ); - // initializations for the sound card names (default) lNumDevs = 1; strDriverNames[0] = strSystemDriverTechniqueName; - // set current device strCurDevName = ""; // default device } @@ -235,102 +230,68 @@ QVector CSoundBase::LoadAndInitializeFirstValidDriver ( const bool bOpe /******************************************************************************\ * MIDI handling * \******************************************************************************/ -void CSoundBase::ParseCommandLineArgument ( const QString& strMIDISetup ) + +void CSoundBase::SetMIDIControllerMapping ( int iFaderOffset, + int iFaderCount, + int iPanOffset, + int iPanCount, + int iSoloOffset, + int iSoloCount, + int iMuteOffset, + int iMuteCount, + int iMuteMyselfCC ) { - int iMIDIOffsetFader = 70; // Behringer X-TOUCH: offset of 0x46 - - // parse the server info string according to definition: there is - // the legacy definition with just one or two numbers that only - // provides a definition for the controller offset of the level - // controllers (default 70 for the sake of Behringer X-Touch) - // [MIDI channel];[offset for level] - // - // The more verbose new form is a sequence of offsets for various - // controllers: at the current point, 'f', 'p', 's', and 'm' are - // parsed for fader, pan, solo, mute controllers respectively. - // However, at the current point of time only 'f' and 'p' - // controllers are actually implemented. The syntax for a Korg - // nanoKONTROL2 with 8 fader controllers starting at offset 0 and - // 8 pan controllers starting at offset 16 would be - // - // [MIDI channel];f0*8;p16*8 - // - // Namely a sequence of letters indicating the kind of controller, - // followed by the offset of the first such controller, followed - // by * and a count for number of controllers (if more than 1) - if ( !strMIDISetup.isEmpty() ) + // Clear all previous MIDI mappings + for ( int i = 0; i < aMidiCtls.size(); ++i ) { - // split the different parameter strings - const QStringList slMIDIParams = strMIDISetup.split ( ";" ); + aMidiCtls[i] = { None, 0 }; + } - // [MIDI channel] - if ( slMIDIParams.count() >= 1 ) + // Map fader controllers + for ( int i = 0; i < iFaderCount && i < MAX_NUM_CHANNELS; ++i ) + { + int iCC = iFaderOffset + i; + if ( iCC >= 0 && iCC < 128 ) { - iCtrlMIDIChannel = slMIDIParams[0].toUInt(); + aMidiCtls[iCC] = { Fader, i }; } + } - bool bSimple = true; // Indicates the legacy kind of specifying - // the fader controller offset without an - // indication of the count of controllers - - // [offset for level] - if ( slMIDIParams.count() >= 2 ) + // Map pan controllers + for ( int i = 0; i < iPanCount && i < MAX_NUM_CHANNELS; ++i ) + { + int iCC = iPanOffset + i; + if ( iCC >= 0 && iCC < 128 ) { - int i = slMIDIParams[1].toUInt ( &bSimple ); - // if the second parameter can be parsed as a number, we - // have the legacy specification of controllers. - if ( bSimple ) - iMIDIOffsetFader = i; + aMidiCtls[iCC] = { Pan, i }; } + } - if ( bSimple ) + // Map solo controllers + for ( int i = 0; i < iSoloCount && i < MAX_NUM_CHANNELS; ++i ) + { + int iCC = iSoloOffset + i; + if ( iCC >= 0 && iCC < 128 ) { - // For the legacy specification, we consider every controller - // up to the maximum number of channels (or the maximum - // controller number) a fader. - for ( int i = 0; i < MAX_NUM_CHANNELS; i++ ) - { - if ( i + iMIDIOffsetFader > 127 ) - break; - aMidiCtls[i + iMIDIOffsetFader] = { EMidiCtlType::Fader, i }; - } - return; + aMidiCtls[iCC] = { Solo, i }; } + } - // We have named controllers - - for ( int i = 1; i < slMIDIParams.count(); i++ ) + // Map mute controllers + for ( int i = 0; i < iMuteCount && i < MAX_NUM_CHANNELS; ++i ) + { + int iCC = iMuteOffset + i; + if ( iCC >= 0 && iCC < 128 ) { - QString sParm = slMIDIParams[i].trimmed(); - if ( sParm.isEmpty() ) - continue; - - int iCtrl = QString ( sMidiCtlChar ).indexOf ( sParm[0] ); - if ( iCtrl < 0 ) - continue; - EMidiCtlType eTyp = static_cast ( iCtrl ); - - if ( eTyp == Device ) - { - // save MIDI device name to select - strMIDIDevice = sParm.mid ( 1 ); - } - else - { - const QStringList slP = sParm.mid ( 1 ).split ( '*' ); - int iFirst = slP[0].toUInt(); - int iNum = ( slP.count() > 1 ) ? slP[1].toUInt() : 1; - for ( int iOff = 0; iOff < iNum; iOff++ ) - { - if ( iOff >= MAX_NUM_CHANNELS ) - break; - if ( iFirst + iOff >= 128 ) - break; - aMidiCtls[iFirst + iOff] = { eTyp, iOff }; - } - } + aMidiCtls[iCC] = { Mute, i }; } } + + // Map mute myself controller + if ( iMuteMyselfCC >= 0 && iMuteMyselfCC < 128 ) + { + aMidiCtls[iMuteMyselfCC] = { MuteMyself, 0 }; + } } void CSoundBase::ParseMIDIMessage ( const CVector& vMIDIPaketBytes ) @@ -367,7 +328,9 @@ void CSoundBase::ParseMIDIMessage ( const CVector& vMIDIPaketBytes ) { const CMidiCtlEntry& cCtrl = aMidiCtls[vMIDIPaketBytes[1]]; const int iValue = vMIDIPaketBytes[2]; - ; + + emit MidiCCReceived ( vMIDIPaketBytes[1] ); + switch ( cCtrl.eType ) { case Fader: diff --git a/src/sound/soundbase.h b/src/sound/soundbase.h index df08b679c0..b0d7f877cb 100644 --- a/src/sound/soundbase.h +++ b/src/sound/soundbase.h @@ -65,12 +65,10 @@ class CMidiCtlEntry class CSoundBase : public QThread { Q_OBJECT - public: CSoundBase ( const QString& strNewSystemDriverTechniqueName, void ( *fpNewProcessCallback ) ( CVector& psData, void* pParg ), - void* pParg, - const QString& strMIDISetup ); + void* pParg ); virtual int Init ( const int iNewPrefMonoBufferSize ) { return iNewPrefMonoBufferSize; } virtual void Start() @@ -108,6 +106,8 @@ class CSoundBase : public QThread virtual void OpenDriverSetup() {} virtual const QString& GetMIDIDevice() { return strMIDIDevice; } + virtual void SetMIDIDevice ( const QString& strDevice ) { strMIDIDevice = strDevice; } + virtual QStringList GetMIDIDevNames() { return QStringList(); } // Base class default (overridden by platform implementations) bool IsRunning() const { return bRun; } bool IsCallbackEntered() const { return bCallbackEntered; } @@ -117,7 +117,21 @@ class CSoundBase : public QThread void EmitReinitRequestSignal ( const ESndCrdResetType eSndCrdResetType ) { emit ReinitRequest ( eSndCrdResetType ); } // this needs to be public so that it can be called from CMidi - void ParseMIDIMessage ( const CVector& vMIDIPaketBytes ); + void ParseMIDIMessage ( const CVector& vMIDIPaketBytes ); + virtual void EnableMIDI ( bool /* bEnable */ ) {} // Default empty implementation + virtual bool IsMIDIEnabled() const { return false; } // Default false + + void SetCtrlMIDIChannel ( int iCh ) { iCtrlMIDIChannel = iCh; } + + void SetMIDIControllerMapping ( int iFaderOffset, + int iFaderCount, + int iPanOffset, + int iPanCount, + int iSoloOffset, + int iSoloCount, + int iMuteOffset, + int iMuteCount, + int iMuteMyselfCC ); protected: virtual QString LoadAndInitializeDriver ( QString, bool ) { return ""; } @@ -179,4 +193,5 @@ class CSoundBase : public QThread void ControllerInFaderIsSolo ( int iChannelIdx, bool bIsSolo ); void ControllerInFaderIsMute ( int iChannelIdx, bool bIsMute ); void ControllerInMuteMyself ( bool bMute ); + void MidiCCReceived ( int ccNumber ); };