From 45b8b58320a82f9b453878507cd7327c37eb0907 Mon Sep 17 00:00:00 2001 From: ignotus666 Date: Thu, 22 May 2025 12:41:28 +0200 Subject: [PATCH 1/3] Add MIDI GUI tab and learn function --- docs/JSON-RPC.md | 34 ++ src/audiomixerboard.cpp | 3 + src/client.cpp | 6 + src/client.h | 10 + src/clientdlg.cpp | 18 + src/clientdlg.h | 2 + src/clientrpc.cpp | 76 ++- src/clientrpc.h | 4 +- src/clientsettingsdlg.cpp | 199 +++++++- src/clientsettingsdlg.h | 23 + src/clientsettingsdlgbase.ui | 735 +++++++++++++++++++++++++++++- src/global.h | 1 + src/main.cpp | 16 +- src/settings.cpp | 87 +++- src/settings.h | 14 + src/sound/asio/sound.cpp | 24 + src/sound/asio/sound.h | 7 + src/sound/coreaudio-mac/sound.cpp | 107 ++++- src/sound/coreaudio-mac/sound.h | 12 +- src/sound/jack/sound.cpp | 66 ++- src/sound/jack/sound.h | 4 + src/sound/midi-win/midi.cpp | 24 +- src/sound/midi-win/midi.h | 4 +- src/sound/soundbase.cpp | 79 +++- src/sound/soundbase.h | 15 +- 25 files changed, 1520 insertions(+), 50 deletions(-) diff --git a/docs/JSON-RPC.md b/docs/JSON-RPC.md index 84271b66a8..d00f5bf377 100644 --- a/docs/JSON-RPC.md +++ b/docs/JSON-RPC.md @@ -188,6 +188,23 @@ Results: | result.clients | array | The client list. See jamulusclient/clientListReceived for the format. | +### 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 +257,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..cda735384c 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -173,6 +173,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(); @@ -1547,6 +1549,10 @@ void CClient::FreeClientChannel ( const int iServerChannelID ) */ } +void CClient::ApplyMIDIMapping ( const QString& midiMap ) { Sound.SetMIDIMapping ( midiMap ); } + +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..48d14ad2ed 100644 --- a/src/client.h +++ b/src/client.h @@ -293,10 +293,16 @@ 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(); } + // settings CChannelCoreInfo ChannelInfo; QString strClientName; + void ApplyMIDIMapping ( const QString& midiMap ); + protected: // callback function must be static, otherwise it does not work static void AudioCallback ( CVector& psData, void* arg ); @@ -473,4 +479,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..276c88d0dc 100644 --- a/src/clientdlg.cpp +++ b/src/clientdlg.cpp @@ -401,6 +401,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 +542,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 +1534,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..e07499161c 100644 --- a/src/clientdlg.h +++ b/src/clientdlg.h @@ -246,6 +246,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..eff7ec2383 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,78 @@ 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 }, + { "midiChannel", m_pSettings->midiChannel }, + { "midiMuteMyself", m_pSettings->midiMuteMyself }, + { "midiFaderOffset", m_pSettings->midiFaderOffset }, + { "midiFaderCount", m_pSettings->midiFaderCount }, + { "midiPanOffset", m_pSettings->midiPanOffset }, + { "midiPanCount", m_pSettings->midiPanCount }, + { "midiSoloOffset", m_pSettings->midiSoloOffset }, + { "midiSoloCount", m_pSettings->midiSoloCount }, + { "midiMuteOffset", m_pSettings->midiMuteOffset }, + { "midiMuteCount", m_pSettings->midiMuteCount } }; + 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 ) { + if ( params.contains ( "bUseMIDIController" ) ) + { + m_pSettings->bUseMIDIController = params["bUseMIDIController"].toBool(); + } + if ( params.contains ( "midiChannel" ) ) + { + m_pSettings->midiChannel = params["midiChannel"].toInt(); + } + if ( params.contains ( "midiMuteMyself" ) ) + { + m_pSettings->midiMuteMyself = params["midiMuteMyself"].toInt(); + } + if ( params.contains ( "midiFaderOffset" ) ) + { + m_pSettings->midiFaderOffset = params["midiFaderOffset"].toInt(); + } + if ( params.contains ( "midiFaderCount" ) ) + { + m_pSettings->midiFaderCount = params["midiFaderCount"].toInt(); + } + if ( params.contains ( "midiPanOffset" ) ) + { + m_pSettings->midiPanOffset = params["midiPanOffset"].toInt(); + } + if ( params.contains ( "midiPanCount" ) ) + { + m_pSettings->midiPanCount = params["midiPanCount"].toInt(); + } + if ( params.contains ( "midiSoloOffset" ) ) + { + m_pSettings->midiSoloOffset = params["midiSoloOffset"].toInt(); + } + if ( params.contains ( "midiSoloCount" ) ) + { + m_pSettings->midiSoloCount = params["midiSoloCount"].toInt(); + } + if ( params.contains ( "midiMuteOffset" ) ) + { + m_pSettings->midiMuteOffset = params["midiMuteOffset"].toInt(); + } + if ( params.contains ( "midiMuteCount" ) ) + { + m_pSettings->midiMuteCount = params["midiMuteCount"].toInt(); + } + response["result"] = "ok"; + } ); } 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..2018941dc8 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,43 @@ 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" ) ); + + 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 ); + lblMuteMyself->setWhatsThis ( strMidiSettings ); + faderGroup->setWhatsThis ( strMidiSettings ); + panGroup->setWhatsThis ( strMidiSettings ); + soloGroup->setWhatsThis ( strMidiSettings ); + muteGroup->setWhatsThis ( strMidiSettings ); + + spnChannel->setAccessibleName ( tr ( "MIDI channel spin 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 +784,63 @@ 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[] = { { spnChannel, &CClientSettings::midiChannel }, + { spnMuteMyself, &CClientSettings::midiMuteMyself }, + { spnFaderOffset, &CClientSettings::midiFaderOffset }, + { spnFaderCount, &CClientSettings::midiFaderCount }, + { spnPanOffset, &CClientSettings::midiPanOffset }, + { spnPanCount, &CClientSettings::midiPanCount }, + { spnSoloOffset, &CClientSettings::midiSoloOffset }, + { spnSoloCount, &CClientSettings::midiSoloCount }, + { spnMuteOffset, &CClientSettings::midiMuteOffset }, + { spnMuteCount, &CClientSettings::midiMuteCount } }; + + for ( const MidiSpinBoxMapping& mapping : midiMappings ) + { + QObject::connect ( mapping.spinBox, static_cast ( &QSpinBox::valueChanged ), this, [this, mapping] ( int v ) { + pSettings->*( mapping.member ) = v; + ApplyMIDIMappingFromSettings(); + } ); + } + + QObject::connect ( chbUseMIDIController, &QCheckBox::toggled, this, [this] ( bool checked ) { + pSettings->bUseMIDIController = checked; + SetMIDIControlsEnabled ( checked ); + + if ( checked ) + { + pClient->ApplyMIDIMapping ( pSettings->GetMIDIMapString() ); + } + else + { + pClient->ApplyMIDIMapping ( "" ); + } + + emit MIDIControllerUsageChanged ( checked ); + } ); + + // 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 +850,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 +869,26 @@ void CClientSettingsDlg::showEvent ( QShowEvent* ) // select the skill level pcbxSkill->setCurrentIndex ( pcbxSkill->findData ( static_cast ( pClient->ChannelInfo.eSkillLevel ) ) ); + + // MIDI tab: set widgets from settings + spnChannel->setValue ( pSettings->midiChannel ); + spnMuteMyself->setValue ( pSettings->midiMuteMyself ); + spnFaderOffset->setValue ( pSettings->midiFaderOffset ); + spnFaderCount->setValue ( pSettings->midiFaderCount ); + spnPanOffset->setValue ( pSettings->midiPanOffset ); + spnPanCount->setValue ( pSettings->midiPanCount ); + spnSoloOffset->setValue ( pSettings->midiSoloOffset ); + spnSoloCount->setValue ( pSettings->midiSoloCount ); + spnMuteOffset->setValue ( pSettings->midiMuteOffset ); + spnMuteCount->setValue ( pSettings->midiMuteCount ); + chbUseMIDIController->setChecked ( pSettings->bUseMIDIController ); + + SetMIDIControlsEnabled ( chbUseMIDIController->isChecked() ); + + // Emit MIDIControllerUsageChanged signal to propagate MIDI state at startup + emit MIDIControllerUsageChanged ( chbUseMIDIController->isChecked() ); + + QDialog::showEvent ( event ); } void CClientSettingsDlg::UpdateJitterBufferFrame() @@ -1216,3 +1331,83 @@ void CClientSettingsDlg::OnAudioPanValueChanged ( int value ) pClient->SetAudioInFader ( value ); UpdateAudioFaderSlider(); } + +void CClientSettingsDlg::ApplyMIDIMappingFromSettings() +{ + // Only apply MIDI mapping if the controller is enabled + if ( pSettings->bUseMIDIController ) + { + pClient->ApplyMIDIMapping ( pSettings->GetMIDIMapString() ); + } + else + { + // If disabled, ensure no MIDI mapping is applied + pClient->ApplyMIDIMapping ( "" ); + } +} + +void CClientSettingsDlg::ResetMidiLearn() +{ + midiLearnTarget = None; + for ( QPushButton* button : midiLearnButtons ) + { + button->setText ( tr ( "Learn" ) ); + button->setEnabled ( true ); + } +} + +void CClientSettingsDlg::SetMIDIControlsEnabled ( bool enabled ) { midiControlsContainer->setEnabled ( enabled ); } + +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..a93b2bffe1 100644 --- a/src/clientsettingsdlg.h +++ b/src/clientsettingsdlg.h @@ -67,6 +67,7 @@ class CClientSettingsDlg : public CBaseDlg, private Ui_CClientSettingsDlgBase void UpdateSoundCardFrame(); void UpdateDirectoryComboBox(); void UpdateAudioFaderSlider(); + void ApplyMIDIMappingFromSettings(); QString GenSndCrdBufferDelayString ( const int iFrameSize, const QString strAddText = "" ); virtual void showEvent ( QShowEvent* ); @@ -119,4 +120,26 @@ 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 ); + +private slots: + void OnLearnButtonClicked(); + void OnMidiCCReceived ( int ccNumber ); }; diff --git a/src/clientsettingsdlgbase.ui b/src/clientsettingsdlgbase.ui index 7ee311701a..ac08206a73 100644 --- a/src/clientsettingsdlgbase.ui +++ b/src/clientsettingsdlgbase.ui @@ -6,8 +6,8 @@ 0 0 - 436 - 524 + 543 + 569 @@ -24,13 +24,13 @@ - + 0 0 - 1 + 3 true @@ -1336,6 +1336,717 @@ + + + MIDI Control + + + + + + + + + + + + + 50 + false + + + + Mute + + + + + + + 50 + false + + + + First MIDI CC + + + + + + + + 65 + 0 + + + + + 50 + false + + + + 127 + + + + + + + + 50 + false + + + + Learn + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 50 + false + + + + Count + + + + + + + + 65 + 0 + + + + + 50 + false + + + + 1 + + + 127 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + 50 + false + + + + Solo + + + + + + + 50 + false + + + + First MIDI CC + + + + + + + + 65 + 0 + + + + + 50 + false + + + + 127 + + + + + + + + 50 + false + + + + Learn + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 50 + false + + + + Count + + + + + + + + 65 + 0 + + + + + 50 + false + + + + 1 + + + 127 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 50 + false + + + + Pan + + + + + + + 50 + false + + + + First MIDI CC + + + + + + + + 65 + 0 + + + + + 50 + false + + + + 127 + + + + + + + + 50 + false + + + + Learn + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 50 + false + + + + Count + + + + + + + + 65 + 0 + + + + + 50 + false + + + + 1 + + + 127 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 75 + true + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 50 + false + + + + MIDI Channel + + + + + + + + 55 + 0 + + + + + 50 + false + + + + 16 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 50 + false + + + + Mute Myself + + + + + + + + 65 + 0 + + + + + 50 + false + + + + 127 + + + + + + + + 50 + false + + + + Learn + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 50 + false + + + + Fader + + + + + + + 50 + false + + + + First MIDI CC + + + + + + + + 65 + 0 + + + + + 50 + false + + + + 127 + + + + + + + + 50 + false + + + + Learn + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 50 + false + + + + Count + + + + + + + + 65 + 0 + + + + + 50 + false + + + + 1 + + + 127 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + 0 + 0 + + + + MIDI-in + + + + + @@ -1379,6 +2090,22 @@ cbxInputBoost chbDetectFeedback sldAudioPan + chbUseMIDIController + spnChannel + spnMuteMyself + butLearnMuteMyself + spnFaderOffset + butLearnFaderOffset + spnFaderCount + spnPanOffset + butLearnPanOffset + spnPanCount + spnSoloOffset + butLearnSoloOffset + spnSoloCount + 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..b80d05d0a5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -920,25 +920,31 @@ int main ( int argc, char** argv ) #ifndef SERVER_ONLY if ( bIsClient ) { - // Client: - // actual client object + // Create client with empty MIDI string initially (safer initialization) CClient Client ( iPortNumber, iQosNumber, strConnOnStartupAddress, - strMIDISetup, + "", // Always start with empty MIDI 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 ); + // Parse command line MIDI parameters if provided + CSoundBase::ParseMIDICommandLineParams ( strMIDISetup, Settings ); + if ( Settings.bUseMIDIController ) + { + Client.ApplyMIDIMapping ( Settings.GetMIDIMapString() ); + } + # ifndef NO_JSON_RPC if ( pRpcServer ) { - new CClientRpc ( &Client, pRpcServer, pRpcServer ); + new CClientRpc ( &Client, &Settings, pRpcServer, pRpcServer ); } # endif diff --git a/src/settings.cpp b/src/settings.cpp index 35c1f409a9..afd11cc6ac 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -461,6 +461,62 @@ void CClientSettings::ReadSettingsFromXML ( const QDomDocument& IniXMLDocument, pClient->SetAudioQuality ( static_cast ( iValue ) ); } + // MIDI settings + if ( GetNumericIniSet ( IniXMLDocument, "client", "midichannel", 0, 16, iValue ) ) + { + midiChannel = iValue; + } + + if ( GetNumericIniSet ( IniXMLDocument, "client", "midifaderoffset", 0, 127, iValue ) ) + { + midiFaderOffset = iValue; + } + + if ( GetNumericIniSet ( IniXMLDocument, "client", "midifadercount", 0, 127, iValue ) ) + { + midiFaderCount = iValue; + } + + if ( GetNumericIniSet ( IniXMLDocument, "client", "midipanoffset", 0, 127, iValue ) ) + { + midiPanOffset = iValue; + } + + if ( GetNumericIniSet ( IniXMLDocument, "client", "midipancount", 0, 127, iValue ) ) + { + midiPanCount = iValue; + } + + if ( GetNumericIniSet ( IniXMLDocument, "client", "midisolooffset", 0, 127, iValue ) ) + { + midiSoloOffset = iValue; + } + + if ( GetNumericIniSet ( IniXMLDocument, "client", "midisolocount", 0, 127, iValue ) ) + { + midiSoloCount = iValue; + } + + if ( GetNumericIniSet ( IniXMLDocument, "client", "midimuteoffset", 0, 127, iValue ) ) + { + midiMuteOffset = iValue; + } + + if ( GetNumericIniSet ( IniXMLDocument, "client", "midimutecount", 0, 127, iValue ) ) + { + midiMuteCount = iValue; + } + + if ( GetNumericIniSet ( IniXMLDocument, "client", "midimutemyself", 0, 127, iValue ) ) + { + midiMuteMyself = iValue; + } + + if ( GetFlagIniSet ( IniXMLDocument, "client", "usemidicontroller", bValue ) ) + { + bUseMIDIController = bValue; + } + // custom directories //### TODO: BEGIN ###// @@ -548,7 +604,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 +612,6 @@ void CClientSettings::ReadSettingsFromXML ( const QDomDocument& IniXMLDocument, // fader settings ReadFaderSettingsFromXML ( IniXMLDocument ); } - void CClientSettings::ReadFaderSettingsFromXML ( const QDomDocument& IniXMLDocument ) { int iIdx; @@ -754,10 +809,38 @@ void CClientSettings::WriteSettingsToXML ( QDomDocument& IniXMLDocument, bool is // Settings Tab SetNumericIniSet ( IniXMLDocument, "client", "settingstab", iSettingsTab ); + // MIDI settings + SetNumericIniSet ( IniXMLDocument, "client", "midichannel", midiChannel ); + SetNumericIniSet ( IniXMLDocument, "client", "midifaderoffset", midiFaderOffset ); + SetNumericIniSet ( IniXMLDocument, "client", "midifadercount", midiFaderCount ); + SetNumericIniSet ( IniXMLDocument, "client", "midipanoffset", midiPanOffset ); + SetNumericIniSet ( IniXMLDocument, "client", "midipancount", midiPanCount ); + SetNumericIniSet ( IniXMLDocument, "client", "midisolooffset", midiSoloOffset ); + SetNumericIniSet ( IniXMLDocument, "client", "midisolocount", midiSoloCount ); + SetNumericIniSet ( IniXMLDocument, "client", "midimuteoffset", midiMuteOffset ); + SetNumericIniSet ( IniXMLDocument, "client", "midimutecount", midiMuteCount ); + SetNumericIniSet ( IniXMLDocument, "client", "midimutemyself", midiMuteMyself ); + SetFlagIniSet ( IniXMLDocument, "client", "usemidicontroller", bUseMIDIController ); + // fader settings WriteFaderSettingsToXML ( IniXMLDocument ); } +QString CClientSettings::GetMIDIMapString() const +{ + return QString ( "%1;f%2*%3;p%4*%5;s%6*%7;m%8*%9;o%10" ) + .arg ( midiChannel ) + .arg ( midiFaderOffset ) + .arg ( midiFaderCount ) + .arg ( midiPanOffset ) + .arg ( midiPanCount ) + .arg ( midiSoloOffset ) + .arg ( midiSoloCount ) + .arg ( midiMuteOffset ) + .arg ( midiMuteCount ) + .arg ( midiMuteMyself ); +} + void CClientSettings::WriteFaderSettingsToXML ( QDomDocument& IniXMLDocument ) { int iIdx; diff --git a/src/settings.h b/src/settings.h index d87854d992..2b4284853b 100644 --- a/src/settings.h +++ b/src/settings.h @@ -201,6 +201,20 @@ class CClientSettings : public CSettings bool bWindowWasShownConnect; bool bOwnFaderFirst; + // MIDI settings + int midiChannel = 0; // Default MIDI channel 0 + int midiMuteMyself = 0; + int midiFaderOffset = 0; + int midiFaderCount = 0; + int midiPanOffset = 0; + int midiPanCount = 0; + int midiSoloOffset = 0; + int midiSoloCount = 0; + int midiMuteOffset = 0; + int midiMuteCount = 0; + bool bUseMIDIController = false; + QString GetMIDIMapString() const; + 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..8e83343252 100644 --- a/src/sound/asio/sound.cpp +++ b/src/sound/asio/sound.cpp @@ -629,6 +629,30 @@ 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(); + bMidiEnabled = true; + } + } + else + { + // Disable MIDI only if it's currently enabled + if ( bMidiEnabled ) + { + Midi.MidiStop(); + bMidiEnabled = false; + } + } +} + +bool CSound::IsMIDIEnabled() const { return bMidiEnabled; } + void CSound::bufferSwitch ( long index, ASIOBool ) { int iCurSample; diff --git a/src/sound/asio/sound.h b/src/sound/asio/sound.h index 3e11482c96..adaed6967c 100644 --- a/src/sound/asio/sound.h +++ b/src/sound/asio/sound.h @@ -82,6 +82,10 @@ class CSound : public CSoundBase virtual float GetInOutLatencyMs() { return fInOutLatencyMs; } + // MIDI port toggle + virtual void EnableMIDI ( bool bEnable ); + virtual bool IsMIDIEnabled() const; + protected: virtual QString LoadAndInitializeDriver ( QString strDriverName, bool bOpenDriverSetup ); virtual void UnloadCurrentDriver(); @@ -138,4 +142,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-mac/sound.cpp b/src/sound/coreaudio-mac/sound.cpp index 51e8775f67..d17faafe22 100644 --- a/src/sound/coreaudio-mac/sound.cpp +++ b/src/sound/coreaudio-mac/sound.cpp @@ -31,6 +31,7 @@ CSound::CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* const bool, const QString& ) : CSoundBase ( "CoreAudio", fpNewProcessCallback, arg, strMIDISetup ), + midiClient ( static_cast ( NULL ) ), midiInPortRef ( static_cast ( NULL ) ) { // Apple Mailing Lists: Subject: GUI Apps should set kAudioHardwarePropertyRunLoop @@ -60,23 +61,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 +718,87 @@ 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 ) ); } + +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 all available MIDI sources + const int iNMIDISources = MIDIGetNumberOfSources(); + for ( int i = 0; i < iNMIDISources; i++ ) + { + MIDIEndpointRef src = MIDIGetSource ( i ); + MIDIPortConnectSource ( midiInPortRef, src, NULL ); + } + + qInfo() << "CoreAudio MIDI port created and connected to" << iNMIDISources << "sources"; + } +} + +void CSound::DestroyMIDIPort() +{ + if ( midiInPortRef != static_cast ( NULL ) ) + { + // Disconnect from all sources before disposing + const int iNMIDISources = MIDIGetNumberOfSources(); + for ( int i = 0; i < iNMIDISources; i++ ) + { + MIDIEndpointRef src = MIDIGetSource ( i ); + MIDIPortDisconnectSource ( midiInPortRef, src ); + } + + // Dispose of the MIDI input port + 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..1f30c44d43 100644 --- a/src/sound/coreaudio-mac/sound.h +++ b/src/sound/coreaudio-mac/sound.h @@ -44,6 +44,8 @@ class CSound : public CSoundBase const bool, const QString& ); + virtual ~CSound(); + virtual int Init ( const int iNewPrefMonoBufferSize ); virtual void Start(); virtual void Stop(); @@ -63,6 +65,10 @@ 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; + // these variables/functions should be protected but cannot since we want // to access them from the callback function CVector vecsTmpAudioSndCrdStereo; @@ -108,6 +114,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 +135,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..050c40490e 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,56 @@ 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 ( 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 ); } + +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"; + } + } +} + +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..a28d5a70f2 100644 --- a/src/sound/jack/sound.h +++ b/src/sound/jack/sound.h @@ -87,6 +87,8 @@ class CSound : public CSoundBase virtual void Stop(); virtual float GetInOutLatencyMs() { return fInOutLatencyMs; } + virtual void EnableMIDI ( bool bEnable ) override; + virtual bool IsMIDIEnabled() const 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 ); 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/soundbase.cpp b/src/sound/soundbase.cpp index a1e077e076..4844b5a59c 100644 --- a/src/sound/soundbase.cpp +++ b/src/sound/soundbase.cpp @@ -23,6 +23,7 @@ \******************************************************************************/ #include "soundbase.h" +#include "../settings.h" // This is used as a lookup table for parsing option letters, mapping // a single character to an EMidiCtlType @@ -235,8 +236,72 @@ QVector CSoundBase::LoadAndInitializeFirstValidDriver ( const bool bOpe /******************************************************************************\ * MIDI handling * \******************************************************************************/ + +void CSoundBase::ParseMIDICommandLineParams ( const QString& strMIDISetup, CClientSettings& Settings ) +{ + if ( !strMIDISetup.isEmpty() ) + { + QStringList slMIDIParams = strMIDISetup.split ( ";" ); + if ( slMIDIParams.count() >= 1 ) + { + Settings.midiChannel = slMIDIParams[0].toInt(); + for ( int i = 1; i < slMIDIParams.count(); ++i ) + { + QString sParm = slMIDIParams[i].trimmed(); + if ( sParm.startsWith ( "f" ) ) + { + QStringList slP = sParm.mid ( 1 ).split ( "*" ); + Settings.midiFaderOffset = slP[0].toInt(); + if ( slP.size() > 1 ) + { + Settings.midiFaderCount = slP[1].toInt(); + } + } + else if ( sParm.startsWith ( "p" ) ) + { + QStringList slP = sParm.mid ( 1 ).split ( "*" ); + Settings.midiPanOffset = slP[0].toInt(); + if ( slP.size() > 1 ) + { + Settings.midiPanCount = slP[1].toInt(); + } + } + else if ( sParm.startsWith ( "s" ) ) + { + QStringList slP = sParm.mid ( 1 ).split ( "*" ); + Settings.midiSoloOffset = slP[0].toInt(); + if ( slP.size() > 1 ) + { + Settings.midiSoloCount = slP[1].toInt(); + } + } + else if ( sParm.startsWith ( "m" ) ) + { + QStringList slP = sParm.mid ( 1 ).split ( "*" ); + Settings.midiMuteOffset = slP[0].toInt(); + if ( slP.size() > 1 ) + { + Settings.midiMuteCount = slP[1].toInt(); + } + } + else if ( sParm.startsWith ( "o" ) ) + { + QStringList slP = sParm.mid ( 1 ).split ( "*" ); + Settings.midiMuteMyself = slP[0].toInt(); + } + } + } + Settings.bUseMIDIController = true; + } +} void CSoundBase::ParseCommandLineArgument ( const QString& strMIDISetup ) { + // Clear all previous MIDI mappings + for ( int i = 0; i < aMidiCtls.size(); ++i ) + { + aMidiCtls[i] = { None, 0 }; + } + int iMIDIOffsetFader = 70; // Behringer X-TOUCH: offset of 0x46 // parse the server info string according to definition: there is @@ -367,7 +432,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: @@ -416,3 +483,13 @@ void CSoundBase::ParseMIDIMessage ( const CVector& vMIDIPaketBytes ) } } } + +void CSoundBase::SetMIDIMapping ( const QString& strMIDISetup ) +{ + // Parse the MIDI mapping + ParseCommandLineArgument ( strMIDISetup ); + + // Enable/disable MIDI port based on whether mapping is empty + bool bShouldEnable = !strMIDISetup.isEmpty(); + EnableMIDI ( bShouldEnable ); +} diff --git a/src/sound/soundbase.h b/src/sound/soundbase.h index df08b679c0..c4db95c820 100644 --- a/src/sound/soundbase.h +++ b/src/sound/soundbase.h @@ -62,11 +62,18 @@ class CMidiCtlEntry }; /* Classes ********************************************************************/ + +#ifndef SERVER_ONLY +class CClientSettings; +#endif + class CSoundBase : public QThread { Q_OBJECT - public: +#ifndef SERVER_ONLY + static void ParseMIDICommandLineParams ( const QString& strMIDISetup, CClientSettings& Settings ); +#endif CSoundBase ( const QString& strNewSystemDriverTechniqueName, void ( *fpNewProcessCallback ) ( CVector& psData, void* pParg ), void* pParg, @@ -117,7 +124,10 @@ 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 ); + void SetMIDIMapping ( const QString& strMIDISetup ); + virtual void EnableMIDI ( bool /* bEnable */ ) {} // Default empty implementation + virtual bool IsMIDIEnabled() const { return false; } // Default false protected: virtual QString LoadAndInitializeDriver ( QString, bool ) { return ""; } @@ -179,4 +189,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 ); }; From 4dacd92efdbe63a75cee5c83c8395cd65e350b79 Mon Sep 17 00:00:00 2001 From: ignotus666 Date: Tue, 3 Feb 2026 11:34:16 +0100 Subject: [PATCH 2/3] Refactor: move MIDI parsing to CSettings; misc. --- src/client.cpp | 34 +++- src/client.h | 14 +- src/clientdlg.cpp | 3 +- src/clientdlg.h | 1 - src/clientrpc.cpp | 40 ++--- src/clientsettingsdlg.cpp | 67 +++----- src/clientsettingsdlg.h | 1 - src/global.h | 2 +- src/main.cpp | 34 +--- src/settings.cpp | 254 +++++++++++++++++++++--------- src/settings.h | 52 ++++-- src/sound/asio/sound.cpp | 8 +- src/sound/asio/sound.h | 2 +- src/sound/coreaudio-ios/sound.h | 6 +- src/sound/coreaudio-ios/sound.mm | 8 +- src/sound/coreaudio-mac/sound.cpp | 8 +- src/sound/coreaudio-mac/sound.h | 6 +- src/sound/jack/sound.cpp | 2 +- src/sound/jack/sound.h | 11 +- src/sound/oboe/sound.cpp | 8 +- src/sound/oboe/sound.h | 6 +- src/sound/soundbase.cpp | 197 +++++------------------ src/sound/soundbase.h | 20 ++- 23 files changed, 385 insertions(+), 399 deletions(-) diff --git a/src/client.cpp b/src/client.cpp index cda735384c..1eedce15a7 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; @@ -195,6 +196,29 @@ CClient::CClient ( const quint16 iPortNumber, } } +// MIDI setup will be handled after settings are assigned +void CClient::ApplyMidiSettingsFromConfig() +{ + if ( pSettings ) + { + Sound.SetCtrlMIDIChannel ( pSettings->iMidiChannel ); + Sound.SetMIDIControllerMapping ( pSettings->iMidiFaderOffset, + pSettings->iMidiFaderCount, + pSettings->iMidiPanOffset, + pSettings->iMidiPanCount, + pSettings->iMidiSoloOffset, + pSettings->iMidiSoloCount, + pSettings->iMidiMuteOffset, + pSettings->iMidiMuteCount, + pSettings->iMidiMuteMyself ); + Sound.EnableMIDI ( pSettings->bUseMIDIController ); + if ( !pSettings->strMidiDevice.isEmpty() ) + { + Sound.SetMIDIDevice ( pSettings->strMidiDevice ); + } + } +} + CClient::~CClient() { // if we were running, stop sound device @@ -1549,8 +1573,6 @@ void CClient::FreeClientChannel ( const int iServerChannelID ) */ } -void CClient::ApplyMIDIMapping ( const QString& midiMap ) { Sound.SetMIDIMapping ( midiMap ); } - void CClient::OnMidiCCReceived ( int ccNumber ) { emit MidiCCReceived ( ccNumber ); } // find, and optionally create, a client channel for the supplied server channel ID diff --git a/src/client.h b/src/client.h index 48d14ad2ed..3b4f648a79 100644 --- a/src/client.h +++ b/src/client.h @@ -127,7 +127,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, @@ -301,9 +300,18 @@ class CClient : public QObject CChannelCoreInfo ChannelInfo; QString strClientName; - void ApplyMIDIMapping ( const QString& midiMap ); +public: + // Assign settings pointer + void SetSettings ( CClientSettings* settings ) { pSettings = settings; } + + // Apply MIDI settings from config + void ApplyMidiSettingsFromConfig(); 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 ); @@ -413,8 +421,6 @@ class CClient : public QObject int maxGainOrPanId; int iCurPingTime; - CSignalHandler* pSignalHandler; - protected slots: void OnHandledSignal ( int sigNum ); void OnSendProtMessage ( CVector vecMessage ); diff --git a/src/clientdlg.cpp b/src/clientdlg.cpp index 276c88d0dc..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(); diff --git a/src/clientdlg.h b/src/clientdlg.h index e07499161c..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, diff --git a/src/clientrpc.cpp b/src/clientrpc.cpp index eff7ec2383..832a85aa4e 100644 --- a/src/clientrpc.cpp +++ b/src/clientrpc.cpp @@ -364,16 +364,16 @@ CClientRpc::CClientRpc ( CClient* pClient, CClientSettings* pSettings, CRpcServe /// @result {object} result - MIDI settings object. pRpcServer->HandleMethod ( "jamulusclient/getMidiSettings", [=] ( const QJsonObject& params, QJsonObject& response ) { QJsonObject jsonMidiParams{ { "bUseMIDIController", m_pSettings->bUseMIDIController }, - { "midiChannel", m_pSettings->midiChannel }, - { "midiMuteMyself", m_pSettings->midiMuteMyself }, - { "midiFaderOffset", m_pSettings->midiFaderOffset }, - { "midiFaderCount", m_pSettings->midiFaderCount }, - { "midiPanOffset", m_pSettings->midiPanOffset }, - { "midiPanCount", m_pSettings->midiPanCount }, - { "midiSoloOffset", m_pSettings->midiSoloOffset }, - { "midiSoloCount", m_pSettings->midiSoloCount }, - { "midiMuteOffset", m_pSettings->midiMuteOffset }, - { "midiMuteCount", m_pSettings->midiMuteCount } }; + { "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 ); } ); @@ -389,43 +389,43 @@ CClientRpc::CClientRpc ( CClient* pClient, CClientSettings* pSettings, CRpcServe } if ( params.contains ( "midiChannel" ) ) { - m_pSettings->midiChannel = params["midiChannel"].toInt(); + m_pSettings->iMidiChannel = params["midiChannel"].toInt(); } if ( params.contains ( "midiMuteMyself" ) ) { - m_pSettings->midiMuteMyself = params["midiMuteMyself"].toInt(); + m_pSettings->iMidiMuteMyself = params["midiMuteMyself"].toInt(); } if ( params.contains ( "midiFaderOffset" ) ) { - m_pSettings->midiFaderOffset = params["midiFaderOffset"].toInt(); + m_pSettings->iMidiFaderOffset = params["midiFaderOffset"].toInt(); } if ( params.contains ( "midiFaderCount" ) ) { - m_pSettings->midiFaderCount = params["midiFaderCount"].toInt(); + m_pSettings->iMidiFaderCount = params["midiFaderCount"].toInt(); } if ( params.contains ( "midiPanOffset" ) ) { - m_pSettings->midiPanOffset = params["midiPanOffset"].toInt(); + m_pSettings->iMidiPanOffset = params["midiPanOffset"].toInt(); } if ( params.contains ( "midiPanCount" ) ) { - m_pSettings->midiPanCount = params["midiPanCount"].toInt(); + m_pSettings->iMidiPanCount = params["midiPanCount"].toInt(); } if ( params.contains ( "midiSoloOffset" ) ) { - m_pSettings->midiSoloOffset = params["midiSoloOffset"].toInt(); + m_pSettings->iMidiSoloOffset = params["midiSoloOffset"].toInt(); } if ( params.contains ( "midiSoloCount" ) ) { - m_pSettings->midiSoloCount = params["midiSoloCount"].toInt(); + m_pSettings->iMidiSoloCount = params["midiSoloCount"].toInt(); } if ( params.contains ( "midiMuteOffset" ) ) { - m_pSettings->midiMuteOffset = params["midiMuteOffset"].toInt(); + m_pSettings->iMidiMuteOffset = params["midiMuteOffset"].toInt(); } if ( params.contains ( "midiMuteCount" ) ) { - m_pSettings->midiMuteCount = params["midiMuteCount"].toInt(); + m_pSettings->iMidiMuteCount = params["midiMuteCount"].toInt(); } response["result"] = "ok"; } ); diff --git a/src/clientsettingsdlg.cpp b/src/clientsettingsdlg.cpp index 2018941dc8..479d5b31b8 100644 --- a/src/clientsettingsdlg.cpp +++ b/src/clientsettingsdlg.cpp @@ -792,22 +792,23 @@ CClientSettingsDlg::CClientSettingsDlg ( CClient* pNCliP, CClientSettings* pNSet int CClientSettings::*member; }; - const MidiSpinBoxMapping midiMappings[] = { { spnChannel, &CClientSettings::midiChannel }, - { spnMuteMyself, &CClientSettings::midiMuteMyself }, - { spnFaderOffset, &CClientSettings::midiFaderOffset }, - { spnFaderCount, &CClientSettings::midiFaderCount }, - { spnPanOffset, &CClientSettings::midiPanOffset }, - { spnPanCount, &CClientSettings::midiPanCount }, - { spnSoloOffset, &CClientSettings::midiSoloOffset }, - { spnSoloCount, &CClientSettings::midiSoloCount }, - { spnMuteOffset, &CClientSettings::midiMuteOffset }, - { spnMuteCount, &CClientSettings::midiMuteCount } }; + const MidiSpinBoxMapping midiMappings[] = { { spnChannel, &CClientSettings::iMidiChannel }, + { 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; - ApplyMIDIMappingFromSettings(); + // Apply MIDI settings changes immediately + pClient->ApplyMidiSettingsFromConfig(); } ); } @@ -815,14 +816,8 @@ CClientSettingsDlg::CClientSettingsDlg ( CClient* pNCliP, CClientSettings* pNSet pSettings->bUseMIDIController = checked; SetMIDIControlsEnabled ( checked ); - if ( checked ) - { - pClient->ApplyMIDIMapping ( pSettings->GetMIDIMapString() ); - } - else - { - pClient->ApplyMIDIMapping ( "" ); - } + // Apply MIDI enable/disable immediately + pClient->ApplyMidiSettingsFromConfig(); emit MIDIControllerUsageChanged ( checked ); } ); @@ -871,16 +866,16 @@ void CClientSettingsDlg::showEvent ( QShowEvent* event ) pcbxSkill->setCurrentIndex ( pcbxSkill->findData ( static_cast ( pClient->ChannelInfo.eSkillLevel ) ) ); // MIDI tab: set widgets from settings - spnChannel->setValue ( pSettings->midiChannel ); - spnMuteMyself->setValue ( pSettings->midiMuteMyself ); - spnFaderOffset->setValue ( pSettings->midiFaderOffset ); - spnFaderCount->setValue ( pSettings->midiFaderCount ); - spnPanOffset->setValue ( pSettings->midiPanOffset ); - spnPanCount->setValue ( pSettings->midiPanCount ); - spnSoloOffset->setValue ( pSettings->midiSoloOffset ); - spnSoloCount->setValue ( pSettings->midiSoloCount ); - spnMuteOffset->setValue ( pSettings->midiMuteOffset ); - spnMuteCount->setValue ( pSettings->midiMuteCount ); + spnChannel->setValue ( 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 ); SetMIDIControlsEnabled ( chbUseMIDIController->isChecked() ); @@ -1332,20 +1327,6 @@ void CClientSettingsDlg::OnAudioPanValueChanged ( int value ) UpdateAudioFaderSlider(); } -void CClientSettingsDlg::ApplyMIDIMappingFromSettings() -{ - // Only apply MIDI mapping if the controller is enabled - if ( pSettings->bUseMIDIController ) - { - pClient->ApplyMIDIMapping ( pSettings->GetMIDIMapString() ); - } - else - { - // If disabled, ensure no MIDI mapping is applied - pClient->ApplyMIDIMapping ( "" ); - } -} - void CClientSettingsDlg::ResetMidiLearn() { midiLearnTarget = None; diff --git a/src/clientsettingsdlg.h b/src/clientsettingsdlg.h index a93b2bffe1..24429acff1 100644 --- a/src/clientsettingsdlg.h +++ b/src/clientsettingsdlg.h @@ -67,7 +67,6 @@ class CClientSettingsDlg : public CBaseDlg, private Ui_CClientSettingsDlgBase void UpdateSoundCardFrame(); void UpdateDirectoryComboBox(); void UpdateAudioFaderSlider(); - void ApplyMIDIMappingFromSettings(); QString GenSndCrdBufferDelayString ( const int iFrameSize, const QString strAddText = "" ); virtual void showEvent ( QShowEvent* ); diff --git a/src/global.h b/src/global.h index 46063c84af..8e9b20b31d 100644 --- a/src/global.h +++ b/src/global.h @@ -68,7 +68,7 @@ LED bar: lbr /* Definitions ****************************************************************/ // define this macro to get debug output -//#define _DEBUG_ +// #define _DEBUG_ #undef _DEBUG_ // version and application name (use version from qt prject file) diff --git a/src/main.cpp b/src/main.cpp index b80d05d0a5..a125083638 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,17 +534,10 @@ int main ( int argc, char** argv ) continue; } - // Controller MIDI channel --------------------------------------------- - if ( GetStringArgument ( argc, - argv, - i, - "--ctrlmidich", // no short form - "--ctrlmidich", - strArgument ) ) + // MIDI + if ( GetStringArgument ( argc, argv, i, "", "--ctrlmidich", strArgument ) ) { - strMIDISetup = strArgument; - qInfo() << qUtf8Printable ( QString ( "- MIDI controller settings: %1" ).arg ( strMIDISetup ) ); - CommandLineOptions << "--ctrlmidich"; + CommandLineOptions << ( QString ( "--ctrlmidich=" ) + strArgument ); ClientOnlyOptions << "--ctrlmidich"; continue; } @@ -920,26 +912,13 @@ int main ( int argc, char** argv ) #ifndef SERVER_ONLY if ( bIsClient ) { - // Create client with empty MIDI string initially (safer initialization) - CClient Client ( iPortNumber, - iQosNumber, - strConnOnStartupAddress, - "", // Always start with empty MIDI - bNoAutoJackConnect, - strClientName, - bEnableIPv6, - bMuteMeInPersonalMix ); + CClient Client ( iPortNumber, iQosNumber, strConnOnStartupAddress, bNoAutoJackConnect, strClientName, bEnableIPv6, bMuteMeInPersonalMix ); // Create Settings with the client pointer CClientSettings Settings ( &Client, strIniFileName ); Settings.Load ( CommandLineOptions ); - - // Parse command line MIDI parameters if provided - CSoundBase::ParseMIDICommandLineParams ( strMIDISetup, Settings ); - if ( Settings.bUseMIDIController ) - { - Client.ApplyMIDIMapping ( Settings.GetMIDIMapString() ); - } + Client.SetSettings ( &Settings ); + Client.ApplyMidiSettingsFromConfig(); # ifndef NO_JSON_RPC if ( pRpcServer ) @@ -962,7 +941,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 afd11cc6ac..b76fc1c2ab 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -200,6 +200,124 @@ void CSettings::PutIniSetting ( QDomDocument& xmlFile, const QString& sSection, xmlKey.appendChild ( currentValue ); } +// Parse MIDI commmand line parameters and update MIDI variables +void CSettings::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& 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; + } + else if ( cType == 'p' ) + { + iMidiPanOffset = iFirst; + iMidiPanCount = iNum; + } + else if ( cType == 's' ) + { + iMidiSoloOffset = iFirst; + iMidiSoloCount = iNum; + } + else if ( cType == 'm' ) + { + iMidiMuteOffset = iFirst; + iMidiMuteCount = iNum; + } + else if ( cType == 'o' ) + { + iMidiMuteMyself = iFirst; + } + } + + bUseMIDIController = true; +} + #ifndef SERVER_ONLY // Client settings ------------------------------------------------------------- void CClientSettings::LoadFaderSettings ( const QString& strCurFileName ) @@ -226,7 +344,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,61 +579,31 @@ void CClientSettings::ReadSettingsFromXML ( const QDomDocument& IniXMLDocument, pClient->SetAudioQuality ( static_cast ( iValue ) ); } - // MIDI settings + // MIDI settings from XML if ( GetNumericIniSet ( IniXMLDocument, "client", "midichannel", 0, 16, iValue ) ) - { - midiChannel = iValue; - } - - if ( GetNumericIniSet ( IniXMLDocument, "client", "midifaderoffset", 0, 127, iValue ) ) - { - midiFaderOffset = 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 ( GetNumericIniSet ( IniXMLDocument, "client", "midifadercount", 0, 127, iValue ) ) - { - midiFaderCount = iValue; - } - - if ( GetNumericIniSet ( IniXMLDocument, "client", "midipanoffset", 0, 127, iValue ) ) - { - midiPanOffset = iValue; - } - - if ( GetNumericIniSet ( IniXMLDocument, "client", "midipancount", 0, 127, iValue ) ) - { - midiPanCount = iValue; - } - - if ( GetNumericIniSet ( IniXMLDocument, "client", "midisolooffset", 0, 127, iValue ) ) - { - midiSoloOffset = iValue; - } - - if ( GetNumericIniSet ( IniXMLDocument, "client", "midisolocount", 0, 127, iValue ) ) - { - midiSoloCount = iValue; - } - - if ( GetNumericIniSet ( IniXMLDocument, "client", "midimuteoffset", 0, 127, iValue ) ) - { - midiMuteOffset = iValue; - } - - if ( GetNumericIniSet ( IniXMLDocument, "client", "midimutecount", 0, 127, iValue ) ) - { - midiMuteCount = iValue; - } - - if ( GetNumericIniSet ( IniXMLDocument, "client", "midimutemyself", 0, 127, iValue ) ) - { - midiMuteMyself = iValue; - } - if ( GetFlagIniSet ( IniXMLDocument, "client", "usemidicontroller", bValue ) ) - { bUseMIDIController = bValue; - } // custom directories @@ -611,6 +699,28 @@ void CClientSettings::ReadSettingsFromXML ( const QDomDocument& IniXMLDocument, // fader settings ReadFaderSettingsFromXML ( IniXMLDocument ); + + // Apply command-line MIDI parameters if present (overwrite .ini values) + for ( const QString& option : CommandLineOptions ) + { + if ( option.startsWith ( "--ctrlmidich=" ) ) + { + QString strMidiMap = option.section ( '=', 1 ); + CSettings::ParseCtrlMidiCh ( strMidiMap, + iMidiChannel, + iMidiFaderOffset, + iMidiFaderCount, + iMidiPanOffset, + iMidiPanCount, + iMidiSoloOffset, + iMidiSoloCount, + iMidiMuteOffset, + iMidiMuteCount, + iMidiMuteMyself, + bUseMIDIController, + &strMidiDevice ); + } + } } void CClientSettings::ReadFaderSettingsFromXML ( const QDomDocument& IniXMLDocument ) { @@ -810,16 +920,16 @@ void CClientSettings::WriteSettingsToXML ( QDomDocument& IniXMLDocument, bool is SetNumericIniSet ( IniXMLDocument, "client", "settingstab", iSettingsTab ); // MIDI settings - SetNumericIniSet ( IniXMLDocument, "client", "midichannel", midiChannel ); - SetNumericIniSet ( IniXMLDocument, "client", "midifaderoffset", midiFaderOffset ); - SetNumericIniSet ( IniXMLDocument, "client", "midifadercount", midiFaderCount ); - SetNumericIniSet ( IniXMLDocument, "client", "midipanoffset", midiPanOffset ); - SetNumericIniSet ( IniXMLDocument, "client", "midipancount", midiPanCount ); - SetNumericIniSet ( IniXMLDocument, "client", "midisolooffset", midiSoloOffset ); - SetNumericIniSet ( IniXMLDocument, "client", "midisolocount", midiSoloCount ); - SetNumericIniSet ( IniXMLDocument, "client", "midimuteoffset", midiMuteOffset ); - SetNumericIniSet ( IniXMLDocument, "client", "midimutecount", midiMuteCount ); - SetNumericIniSet ( IniXMLDocument, "client", "midimutemyself", midiMuteMyself ); + 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 ); // fader settings @@ -829,16 +939,16 @@ void CClientSettings::WriteSettingsToXML ( QDomDocument& IniXMLDocument, bool is QString CClientSettings::GetMIDIMapString() const { return QString ( "%1;f%2*%3;p%4*%5;s%6*%7;m%8*%9;o%10" ) - .arg ( midiChannel ) - .arg ( midiFaderOffset ) - .arg ( midiFaderCount ) - .arg ( midiPanOffset ) - .arg ( midiPanCount ) - .arg ( midiSoloOffset ) - .arg ( midiSoloCount ) - .arg ( midiMuteOffset ) - .arg ( midiMuteCount ) - .arg ( midiMuteMyself ); + .arg ( iMidiChannel ) + .arg ( iMidiFaderOffset ) + .arg ( iMidiFaderCount ) + .arg ( iMidiPanOffset ) + .arg ( iMidiPanCount ) + .arg ( iMidiSoloOffset ) + .arg ( iMidiSoloCount ) + .arg ( iMidiMuteOffset ) + .arg ( iMidiMuteCount ) + .arg ( iMidiMuteMyself ); } void CClientSettings::WriteFaderSettingsToXML ( QDomDocument& IniXMLDocument ) diff --git a/src/settings.h b/src/settings.h index 2b4284853b..ae5ec7d055 100644 --- a/src/settings.h +++ b/src/settings.h @@ -41,9 +41,26 @@ /* Classes ********************************************************************/ class CSettings : public QObject + { Q_OBJECT +public: + // 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& bUseMIDIController, + QString* strMIDIDevice = nullptr ); + public: CSettings() : vecWindowPosMain(), // empty array @@ -164,6 +181,18 @@ 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 ), + bUseMIDIController ( false ), + strMidiDevice ( "" ), pClient ( pNCliP ) { SetFileName ( sNFiName, DEFAULT_INI_FILE_NAME ); @@ -202,17 +231,18 @@ class CClientSettings : public CSettings bool bOwnFaderFirst; // MIDI settings - int midiChannel = 0; // Default MIDI channel 0 - int midiMuteMyself = 0; - int midiFaderOffset = 0; - int midiFaderCount = 0; - int midiPanOffset = 0; - int midiPanCount = 0; - int midiSoloOffset = 0; - int midiSoloCount = 0; - int midiMuteOffset = 0; - int midiMuteCount = 0; - bool bUseMIDIController = false; + int iMidiChannel; + int iMidiMuteMyself; + int iMidiFaderOffset; + int iMidiFaderCount; + int iMidiPanOffset; + int iMidiPanCount; + int iMidiSoloOffset; + int iMidiSoloCount; + int iMidiMuteOffset; + int iMidiMuteCount; + bool bUseMIDIController; + QString strMidiDevice; QString GetMIDIMapString() const; protected: diff --git a/src/sound/asio/sound.cpp b/src/sound/asio/sound.cpp index 8e83343252..6c4a72158a 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 ), diff --git a/src/sound/asio/sound.h b/src/sound/asio/sound.h index adaed6967c..4ba7692163 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(); 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 d17faafe22..28a9a647df 100644 --- a/src/sound/coreaudio-mac/sound.cpp +++ b/src/sound/coreaudio-mac/sound.cpp @@ -25,12 +25,8 @@ #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 ) ) { diff --git a/src/sound/coreaudio-mac/sound.h b/src/sound/coreaudio-mac/sound.h index 1f30c44d43..39d1aab3d1 100644 --- a/src/sound/coreaudio-mac/sound.h +++ b/src/sound/coreaudio-mac/sound.h @@ -38,11 +38,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& ); virtual ~CSound(); diff --git a/src/sound/jack/sound.cpp b/src/sound/jack/sound.cpp index 050c40490e..5971f92eea 100644 --- a/src/sound/jack/sound.cpp +++ b/src/sound/jack/sound.cpp @@ -182,7 +182,7 @@ void CSound::EnableMIDI ( bool bEnable ) { if ( bEnable && ( iCtrlMIDIChannel != INVALID_MIDI_CH ) ) { - // Create MIDI port if we have valid MIDI channel and no port exists + // Create MIDI port if we have a valid MIDI channel and no port exists if ( input_port_midi == nullptr ) { CreateMIDIPort(); diff --git a/src/sound/jack/sound.h b/src/sound/jack/sound.h index a28d5a70f2..2ab2020636 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 ) @@ -125,12 +124,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/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 4844b5a59c..f7c1ce0f4c 100644 --- a/src/sound/soundbase.cpp +++ b/src/sound/soundbase.cpp @@ -40,8 +40,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 ), @@ -50,13 +49,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 } @@ -237,64 +232,15 @@ QVector CSoundBase::LoadAndInitializeFirstValidDriver ( const bool bOpe * MIDI handling * \******************************************************************************/ -void CSoundBase::ParseMIDICommandLineParams ( const QString& strMIDISetup, CClientSettings& Settings ) -{ - if ( !strMIDISetup.isEmpty() ) - { - QStringList slMIDIParams = strMIDISetup.split ( ";" ); - if ( slMIDIParams.count() >= 1 ) - { - Settings.midiChannel = slMIDIParams[0].toInt(); - for ( int i = 1; i < slMIDIParams.count(); ++i ) - { - QString sParm = slMIDIParams[i].trimmed(); - if ( sParm.startsWith ( "f" ) ) - { - QStringList slP = sParm.mid ( 1 ).split ( "*" ); - Settings.midiFaderOffset = slP[0].toInt(); - if ( slP.size() > 1 ) - { - Settings.midiFaderCount = slP[1].toInt(); - } - } - else if ( sParm.startsWith ( "p" ) ) - { - QStringList slP = sParm.mid ( 1 ).split ( "*" ); - Settings.midiPanOffset = slP[0].toInt(); - if ( slP.size() > 1 ) - { - Settings.midiPanCount = slP[1].toInt(); - } - } - else if ( sParm.startsWith ( "s" ) ) - { - QStringList slP = sParm.mid ( 1 ).split ( "*" ); - Settings.midiSoloOffset = slP[0].toInt(); - if ( slP.size() > 1 ) - { - Settings.midiSoloCount = slP[1].toInt(); - } - } - else if ( sParm.startsWith ( "m" ) ) - { - QStringList slP = sParm.mid ( 1 ).split ( "*" ); - Settings.midiMuteOffset = slP[0].toInt(); - if ( slP.size() > 1 ) - { - Settings.midiMuteCount = slP[1].toInt(); - } - } - else if ( sParm.startsWith ( "o" ) ) - { - QStringList slP = sParm.mid ( 1 ).split ( "*" ); - Settings.midiMuteMyself = slP[0].toInt(); - } - } - } - Settings.bUseMIDIController = true; - } -} -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 ) { // Clear all previous MIDI mappings for ( int i = 0; i < aMidiCtls.size(); ++i ) @@ -302,100 +248,51 @@ void CSoundBase::ParseCommandLineArgument ( const QString& strMIDISetup ) aMidiCtls[i] = { None, 0 }; } - 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() ) + // Map fader controllers + for ( int i = 0; i < iFaderCount && i < MAX_NUM_CHANNELS; ++i ) { - // split the different parameter strings - const QStringList slMIDIParams = strMIDISetup.split ( ";" ); - - // [MIDI channel] - if ( slMIDIParams.count() >= 1 ) + 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 ) @@ -483,13 +380,3 @@ void CSoundBase::ParseMIDIMessage ( const CVector& vMIDIPaketBytes ) } } } - -void CSoundBase::SetMIDIMapping ( const QString& strMIDISetup ) -{ - // Parse the MIDI mapping - ParseCommandLineArgument ( strMIDISetup ); - - // Enable/disable MIDI port based on whether mapping is empty - bool bShouldEnable = !strMIDISetup.isEmpty(); - EnableMIDI ( bShouldEnable ); -} diff --git a/src/sound/soundbase.h b/src/sound/soundbase.h index c4db95c820..2917684df9 100644 --- a/src/sound/soundbase.h +++ b/src/sound/soundbase.h @@ -71,13 +71,9 @@ class CSoundBase : public QThread { Q_OBJECT public: -#ifndef SERVER_ONLY - static void ParseMIDICommandLineParams ( const QString& strMIDISetup, CClientSettings& Settings ); -#endif 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() @@ -115,6 +111,7 @@ class CSoundBase : public QThread virtual void OpenDriverSetup() {} virtual const QString& GetMIDIDevice() { return strMIDIDevice; } + virtual void SetMIDIDevice ( const QString& strDevice ) { strMIDIDevice = strDevice; } bool IsRunning() const { return bRun; } bool IsCallbackEntered() const { return bCallbackEntered; } @@ -125,10 +122,21 @@ class CSoundBase : public QThread // this needs to be public so that it can be called from CMidi void ParseMIDIMessage ( const CVector& vMIDIPaketBytes ); - void SetMIDIMapping ( const QString& strMIDISetup ); 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 ""; } virtual void UnloadCurrentDriver() {} From 94a0a41d544c360833895b27447b583c0259d240 Mon Sep 17 00:00:00 2001 From: ignotus666 Date: Wed, 4 Feb 2026 12:53:40 +0100 Subject: [PATCH 3/3] Move command line reading, cleanup, misc --- src/client.cpp | 38 ++++++++-------- src/client.h | 5 +-- src/main.cpp | 10 +++-- src/settings.cpp | 111 ++++++++++++++++++++++------------------------- src/settings.h | 1 - 5 files changed, 80 insertions(+), 85 deletions(-) diff --git a/src/client.cpp b/src/client.cpp index 1eedce15a7..e915648f74 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -197,25 +197,29 @@ CClient::CClient ( const quint16 iPortNumber, } // MIDI setup will be handled after settings are assigned +void CClient::SetSettings ( CClientSettings* settings ) +{ + pSettings = settings; + ApplyMidiSettingsFromConfig(); +} + void CClient::ApplyMidiSettingsFromConfig() { - if ( pSettings ) - { - Sound.SetCtrlMIDIChannel ( pSettings->iMidiChannel ); - Sound.SetMIDIControllerMapping ( pSettings->iMidiFaderOffset, - pSettings->iMidiFaderCount, - pSettings->iMidiPanOffset, - pSettings->iMidiPanCount, - pSettings->iMidiSoloOffset, - pSettings->iMidiSoloCount, - pSettings->iMidiMuteOffset, - pSettings->iMidiMuteCount, - pSettings->iMidiMuteMyself ); - Sound.EnableMIDI ( pSettings->bUseMIDIController ); - if ( !pSettings->strMidiDevice.isEmpty() ) - { - Sound.SetMIDIDevice ( pSettings->strMidiDevice ); - } + // Apply MIDI settings + Sound.SetCtrlMIDIChannel ( pSettings->iMidiChannel ); + Sound.SetMIDIControllerMapping ( pSettings->iMidiFaderOffset, + pSettings->iMidiFaderCount, + pSettings->iMidiPanOffset, + pSettings->iMidiPanCount, + pSettings->iMidiSoloOffset, + pSettings->iMidiSoloCount, + pSettings->iMidiMuteOffset, + pSettings->iMidiMuteCount, + pSettings->iMidiMuteMyself ); + Sound.EnableMIDI ( pSettings->bUseMIDIController ); + if ( !pSettings->strMidiDevice.isEmpty() ) + { + Sound.SetMIDIDevice ( pSettings->strMidiDevice ); } } diff --git a/src/client.h b/src/client.h index 3b4f648a79..281e50ad4f 100644 --- a/src/client.h +++ b/src/client.h @@ -301,10 +301,7 @@ class CClient : public QObject QString strClientName; public: - // Assign settings pointer - void SetSettings ( CClientSettings* settings ) { pSettings = settings; } - - // Apply MIDI settings from config + void SetSettings ( CClientSettings* settings ); void ApplyMidiSettingsFromConfig(); protected: diff --git a/src/main.cpp b/src/main.cpp index a125083638..7739aa1f30 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -535,9 +535,14 @@ int main ( int argc, char** argv ) } // MIDI - if ( GetStringArgument ( argc, argv, i, "", "--ctrlmidich", strArgument ) ) + if ( GetStringArgument ( argc, + argv, + i, + "--ctrlmidich", // no short form + "--ctrlmidich", + strArgument ) ) { - CommandLineOptions << ( QString ( "--ctrlmidich=" ) + strArgument ); + CommandLineOptions << QString ( "--ctrlmidich=%1" ).arg ( strArgument ); ClientOnlyOptions << "--ctrlmidich"; continue; } @@ -918,7 +923,6 @@ int main ( int argc, char** argv ) CClientSettings Settings ( &Client, strIniFileName ); Settings.Load ( CommandLineOptions ); Client.SetSettings ( &Settings ); - Client.ApplyMidiSettingsFromConfig(); # ifndef NO_JSON_RPC if ( pRpcServer ) diff --git a/src/settings.cpp b/src/settings.cpp index b76fc1c2ab..5de84851be 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -200,6 +200,8 @@ void CSettings::PutIniSetting ( QDomDocument& xmlFile, const QString& sSection, xmlKey.appendChild ( currentValue ); } +#ifndef SERVER_ONLY + // Parse MIDI commmand line parameters and update MIDI variables void CSettings::ParseCtrlMidiCh ( const QString& strMidiMap, int& iMidiChannel, @@ -318,7 +320,6 @@ void CSettings::ParseCtrlMidiCh ( const QString& strMidiMap, bUseMIDIController = true; } -#ifndef SERVER_ONLY // Client settings ------------------------------------------------------------- void CClientSettings::LoadFaderSettings ( const QString& strCurFileName ) { @@ -579,31 +580,58 @@ void CClientSettings::ReadSettingsFromXML ( const QDomDocument& IniXMLDocument, pClient->SetAudioQuality ( static_cast ( iValue ) ); } - // MIDI settings from XML - if ( GetNumericIniSet ( IniXMLDocument, "client", "midichannel", 0, 16, iValue ) ) - iMidiChannel = iValue; - - struct MidiSettingEntry + // MIDI settings: check command line first, then fall back to XML + bool bMidiFromCommandLine = false; + for ( const QString& option : CommandLineOptions ) { - 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 ( option.startsWith ( "--ctrlmidich=" ) ) + { + QString strMidiMap = option.section ( '=', 1 ); + CSettings::ParseCtrlMidiCh ( strMidiMap, + iMidiChannel, + iMidiFaderOffset, + iMidiFaderCount, + iMidiPanOffset, + iMidiPanCount, + iMidiSoloOffset, + iMidiSoloCount, + iMidiMuteOffset, + iMidiMuteCount, + iMidiMuteMyself, + bUseMIDIController, + &strMidiDevice ); + bMidiFromCommandLine = true; + break; + } + } + + if ( !bMidiFromCommandLine ) { - if ( GetNumericIniSet ( IniXMLDocument, "client", entry.key, 0, 127, iValue ) ) - *( entry.variable ) = iValue; + 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; } - if ( GetFlagIniSet ( IniXMLDocument, "client", "usemidicontroller", bValue ) ) - bUseMIDIController = bValue; // custom directories @@ -699,28 +727,6 @@ void CClientSettings::ReadSettingsFromXML ( const QDomDocument& IniXMLDocument, // fader settings ReadFaderSettingsFromXML ( IniXMLDocument ); - - // Apply command-line MIDI parameters if present (overwrite .ini values) - for ( const QString& option : CommandLineOptions ) - { - if ( option.startsWith ( "--ctrlmidich=" ) ) - { - QString strMidiMap = option.section ( '=', 1 ); - CSettings::ParseCtrlMidiCh ( strMidiMap, - iMidiChannel, - iMidiFaderOffset, - iMidiFaderCount, - iMidiPanOffset, - iMidiPanCount, - iMidiSoloOffset, - iMidiSoloCount, - iMidiMuteOffset, - iMidiMuteCount, - iMidiMuteMyself, - bUseMIDIController, - &strMidiDevice ); - } - } } void CClientSettings::ReadFaderSettingsFromXML ( const QDomDocument& IniXMLDocument ) { @@ -936,21 +942,6 @@ void CClientSettings::WriteSettingsToXML ( QDomDocument& IniXMLDocument, bool is WriteFaderSettingsToXML ( IniXMLDocument ); } -QString CClientSettings::GetMIDIMapString() const -{ - return QString ( "%1;f%2*%3;p%4*%5;s%6*%7;m%8*%9;o%10" ) - .arg ( iMidiChannel ) - .arg ( iMidiFaderOffset ) - .arg ( iMidiFaderCount ) - .arg ( iMidiPanOffset ) - .arg ( iMidiPanCount ) - .arg ( iMidiSoloOffset ) - .arg ( iMidiSoloCount ) - .arg ( iMidiMuteOffset ) - .arg ( iMidiMuteCount ) - .arg ( iMidiMuteMyself ); -} - void CClientSettings::WriteFaderSettingsToXML ( QDomDocument& IniXMLDocument ) { int iIdx; diff --git a/src/settings.h b/src/settings.h index ae5ec7d055..f324607511 100644 --- a/src/settings.h +++ b/src/settings.h @@ -243,7 +243,6 @@ class CClientSettings : public CSettings int iMidiMuteCount; bool bUseMIDIController; QString strMidiDevice; - QString GetMIDIMapString() const; protected: virtual void WriteSettingsToXML ( QDomDocument& IniXMLDocument, bool isAboutToQuit ) override;