diff --git a/src/audiomixerboard.cpp b/src/audiomixerboard.cpp index f463088dfc..5f171d63ca 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 0aee733ca4..d2509b3dec 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -172,6 +172,8 @@ CClient::CClient ( const quint16 iPortNumber, QObject::connect ( pSignalHandler, &CSignalHandler::HandledSignal, this, &CClient::OnHandledSignal ); + QObject::connect ( &Sound, &CSoundBase::MidiCCReceived, this, &CClient::OnMidiCCReceived ); + // start timer so that elapsed time works PreciseTime.start(); @@ -1550,6 +1552,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 344db5163d..3bfa3f0139 100644 --- a/src/client.h +++ b/src/client.h @@ -286,15 +286,21 @@ class CClient : public QObject Channel.GetBufErrorRates ( vecErrRates, dLimit, dMaxUpLimit ); } - //### TODO: BEGIN ###// - // Refactor this to use signal/slot mechanism. https://github.com/jamulussoftware/jamulus/pull/3479/files#r1976382416 + // ### TODO: BEGIN ###// + // Refactor this to use signal/slot mechanism. https://github.com/jamulussoftware/jamulus/pull/3479/files#r1976382416 CProtocol* getConnLessProtocol() { return &ConnLessProtocol; } - //### TODO: END ###// + // ### 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 ); @@ -471,4 +477,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 b7048155b1..6fa0c9f049 100644 --- a/src/clientdlg.cpp +++ b/src/clientdlg.cpp @@ -400,6 +400,8 @@ 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, SLOT ( OnOpenMidiSettings() ), QKeySequence ( Qt::CTRL + Qt::Key_M ) ); + // Main menu bar ----------------------------------------------------------- QMenuBar* pMenu = new QMenuBar ( this ); @@ -535,6 +537,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 ); @@ -987,9 +991,8 @@ void CClientDlg::ShowGeneralSettings ( int iTab ) // open general settings dialog emit SendTabChange ( iTab ); ClientSettingsDlg.show(); - ClientSettingsDlg.setWindowTitle ( MakeClientNameTitle ( tr ( "Settings" ), pClient->strClientName ) ); - // make sure dialog is upfront and has focus + ClientSettingsDlg.setWindowTitle ( MakeClientNameTitle ( tr ( "Settings" ), pClient->strClientName ) ); ClientSettingsDlg.raise(); ClientSettingsDlg.activateWindow(); } @@ -1286,11 +1289,11 @@ void CClientDlg::Disconnect() TimerDetectFeedback.stop(); bDetectFeedback = false; - //### TODO: BEGIN ###// - // is this still required??? - // immediately update status bar + // ### TODO: BEGIN ###// + // is this still required??? + // immediately update status bar OnTimerStatus(); - //### TODO: END ###// + // ### TODO: END ###// // reset LEDs ledBuffers->Reset(); @@ -1516,3 +1519,14 @@ void CClientDlg::SetPingTime ( const int iPingTime, const int iOverallDelayMs, c // set current LED status ledDelay->SetLight ( eOverallDelayLEDColor ); } + +void CClientDlg::OnOpenMidiSettings() { ShowGeneralSettings ( SETTING_TAB_MIDI ); } + +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 ba01ea648e..52aa968cd7 100644 --- a/src/clientdlg.h +++ b/src/clientdlg.h @@ -243,6 +243,9 @@ public slots: void accept() { close(); } // introduced by pljones + void OnOpenMidiSettings(); + void OnMIDIControllerUsageChanged ( bool bEnabled ); + signals: void SendTabChange ( int iTabIdx ); }; diff --git a/src/clientsettingsdlg.cpp b/src/clientsettingsdlg.cpp index 07b76a64c2..8d039b3fb8 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 (1-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,87 @@ CClientSettingsDlg::CClientSettingsDlg ( CClient* pNCliP, CClientSettings* pNSet QObject::connect ( pcbxSkill, static_cast ( &QComboBox::activated ), this, &CClientSettingsDlg::OnSkillActivated ); + // MIDI tab + QObject::connect ( spnChannel, static_cast ( &QSpinBox::valueChanged ), this, [this] ( int v ) { + pSettings->midiChannel = v; + ApplyMIDIMappingFromSettings(); + } ); + + QObject::connect ( spnMuteMyself, static_cast ( &QSpinBox::valueChanged ), this, [this] ( int v ) { + pSettings->midiMuteMyself = v; + ApplyMIDIMappingFromSettings(); + } ); + + QObject::connect ( spnFaderOffset, static_cast ( &QSpinBox::valueChanged ), this, [this] ( int v ) { + pSettings->midiFaderOffset = v; + ApplyMIDIMappingFromSettings(); + } ); + + QObject::connect ( spnFaderCount, static_cast ( &QSpinBox::valueChanged ), this, [this] ( int v ) { + pSettings->midiFaderCount = v; + ApplyMIDIMappingFromSettings(); + } ); + + QObject::connect ( spnPanOffset, static_cast ( &QSpinBox::valueChanged ), this, [this] ( int v ) { + pSettings->midiPanOffset = v; + ApplyMIDIMappingFromSettings(); + } ); + + QObject::connect ( spnPanCount, static_cast ( &QSpinBox::valueChanged ), this, [this] ( int v ) { + pSettings->midiPanCount = v; + ApplyMIDIMappingFromSettings(); + } ); + + QObject::connect ( spnSoloOffset, static_cast ( &QSpinBox::valueChanged ), this, [this] ( int v ) { + pSettings->midiSoloOffset = v; + ApplyMIDIMappingFromSettings(); + } ); + + QObject::connect ( spnSoloCount, static_cast ( &QSpinBox::valueChanged ), this, [this] ( int v ) { + pSettings->midiSoloCount = v; + ApplyMIDIMappingFromSettings(); + } ); + + QObject::connect ( spnMuteOffset, static_cast ( &QSpinBox::valueChanged ), this, [this] ( int v ) { + pSettings->midiMuteOffset = v; + ApplyMIDIMappingFromSettings(); + } ); + + QObject::connect ( spnMuteCount, static_cast ( &QSpinBox::valueChanged ), this, [this] ( int v ) { + pSettings->midiMuteCount = 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 +874,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 +893,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 +1355,113 @@ 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() ); + + MidiLearnTarget target = None; + + if ( sender == butLearnMuteMyself ) + { + target = MuteMyself; + } + else if ( sender == butLearnFaderOffset ) + { + target = Fader; + } + else if ( sender == butLearnPanOffset ) + { + target = Pan; + } + else if ( sender == butLearnSoloOffset ) + { + target = Solo; + } + else if ( sender == butLearnMuteOffset ) + { + target = Mute; + } + + SetMidiLearnTarget ( target, 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; + } + + switch ( midiLearnTarget ) + { + case Fader: + spnFaderOffset->setValue ( ccNumber ); + break; + case Pan: + spnPanOffset->setValue ( ccNumber ); + break; + case Solo: + spnSoloOffset->setValue ( ccNumber ); + break; + case Mute: + spnMuteOffset->setValue ( ccNumber ); + break; + case MuteMyself: + spnMuteMyself->setValue ( ccNumber ); + break; + default: + break; + } + ResetMidiLearn(); +} diff --git a/src/clientsettingsdlg.h b/src/clientsettingsdlg.h index 6a6f1cc7d3..822c100317 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 55d2b57683..256b1d3f40 100644 --- a/src/global.h +++ b/src/global.h @@ -266,6 +266,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 cb81b387ca..2904ee67f8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -865,10 +865,10 @@ int main ( int argc, char** argv ) Q_INIT_RESOURCE ( resources ); #ifndef SERVER_ONLY - //### TEST: BEGIN ###// - // activate the following line to activate the test bench, - // CTestbench Testbench ( "127.0.0.1", DEFAULT_PORT_NUMBER ); - //### TEST: END ###// + // ### TEST: BEGIN ###// + // activate the following line to activate the test bench, + // CTestbench Testbench ( "127.0.0.1", DEFAULT_PORT_NUMBER ); + // ### TEST: END ###// #endif #ifdef NO_JSON_RPC @@ -920,21 +920,83 @@ 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 + 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(); + } + } + } + + // Enable MIDI controller and apply settings when command line MIDI is provided + Settings.bUseMIDIController = true; + Client.ApplyMIDIMapping ( Settings.GetMIDIMapString() ); + } + else if ( Settings.bUseMIDIController ) + { + Client.ApplyMIDIMapping ( Settings.GetMIDIMapString() ); + } + # ifndef NO_JSON_RPC if ( pRpcServer ) { diff --git a/src/settings.cpp b/src/settings.cpp index e9ccf90d23..825b2282eb 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -461,19 +461,75 @@ 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 ###// - // compatibility to old version (< 3.6.1) + // ### TODO: BEGIN ###// + // compatibility to old version (< 3.6.1) QString strDirectoryAddress = GetIniSetting ( IniXMLDocument, "client", "centralservaddr", "" ); - //### TODO: END ###// + // ### TODO: END ###// for ( iIdx = 0; iIdx < MAX_NUM_SERVER_ADDR_ITEMS; iIdx++ ) { - //### TODO: BEGIN ###// - // compatibility to old version (< 3.8.2) + // ### TODO: BEGIN ###// + // compatibility to old version (< 3.8.2) strDirectoryAddress = GetIniSetting ( IniXMLDocument, "client", QString ( "centralservaddr%1" ).arg ( iIdx ), strDirectoryAddress ); - //### TODO: END ###// + // ### TODO: END ###// vstrDirectoryAddress[iIdx] = GetIniSetting ( IniXMLDocument, "client", QString ( "directoryaddress%1" ).arg ( iIdx ), strDirectoryAddress ); strDirectoryAddress = ""; @@ -481,9 +537,9 @@ void CClientSettings::ReadSettingsFromXML ( const QDomDocument& IniXMLDocument, // directory type - //### TODO: BEGIN ###// - // compatibility to old version (<3.4.7) - // only the case that "centralservaddr" was set in old ini must be considered + // ### TODO: BEGIN ###// + // compatibility to old version (<3.4.7) + // only the case that "centralservaddr" was set in old ini must be considered if ( !vstrDirectoryAddress[0].isEmpty() && GetFlagIniSet ( IniXMLDocument, "client", "defcentservaddr", bValue ) && !bValue ) { eDirectoryType = AT_CUSTOM; @@ -493,7 +549,7 @@ void CClientSettings::ReadSettingsFromXML ( const QDomDocument& IniXMLDocument, { eDirectoryType = static_cast ( iValue ); } - //### TODO: END ###// + // ### TODO: END ###// else if ( GetNumericIniSet ( IniXMLDocument, "client", "directorytype", 0, static_cast ( AT_CUSTOM ), iValue ) ) { @@ -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; @@ -846,10 +929,10 @@ void CServerSettings::ReadSettingsFromXML ( const QDomDocument& IniXMLDocument, // Server GUI defaults to "" QString directoryAddress = ""; - //### TODO: BEGIN ###// - // compatibility to old version < 3.8.2 + // ### TODO: BEGIN ###// + // compatibility to old version < 3.8.2 directoryAddress = GetIniSetting ( IniXMLDocument, "server", "centralservaddr", directoryAddress ); - //### TODO: END ###// + // ### TODO: END ###// directoryAddress = GetIniSetting ( IniXMLDocument, "server", "directoryaddress", directoryAddress ); @@ -869,20 +952,20 @@ void CServerSettings::ReadSettingsFromXML ( const QDomDocument& IniXMLDocument, } else { - //### TODO: BEGIN ###// - // compatibility to old version < 3.4.7 + // ### TODO: BEGIN ###// + // compatibility to old version < 3.4.7 if ( GetFlagIniSet ( IniXMLDocument, "server", "defcentservaddr", bValue ) ) { directoryType = bValue ? AT_DEFAULT : AT_CUSTOM; } else { - //### TODO: END ###// + // ### TODO: END ###// // if "directorytype" itself is set, use it (note "AT_NONE", "AT_DEFAULT" and "AT_CUSTOM" are min/max directory type here) - //### TODO: BEGIN ###// - // compatibility to old version < 3.8.2 + // ### TODO: BEGIN ###// + // compatibility to old version < 3.8.2 if ( GetNumericIniSet ( IniXMLDocument, "server", "centservaddrtype", @@ -892,7 +975,7 @@ void CServerSettings::ReadSettingsFromXML ( const QDomDocument& IniXMLDocument, { directoryType = static_cast ( iValue ); } - //### TODO: END ###// + // ### TODO: END ###// else { @@ -908,14 +991,14 @@ void CServerSettings::ReadSettingsFromXML ( const QDomDocument& IniXMLDocument, } } - //### TODO: BEGIN ###// - // compatibility to old version < 3.9.0 - // override type to AT_NONE if servlistenabled exists and is false + // ### TODO: BEGIN ###// + // compatibility to old version < 3.9.0 + // override type to AT_NONE if servlistenabled exists and is false if ( GetFlagIniSet ( IniXMLDocument, "server", "servlistenabled", bValue ) && !bValue ) { directoryType = AT_NONE; } - //### TODO: END ###// + // ### TODO: END ###// } pServer->SetDirectoryType ( directoryType ); diff --git a/src/settings.h b/src/settings.h index 7e3a882cc1..3499194fc8 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 74044315c4..a8bec59418 100644 --- a/src/sound/asio/sound.cpp +++ b/src/sound/asio/sound.cpp @@ -311,7 +311,7 @@ int CSound::GetActualBufferSize ( const int iDesiredBufferSizeMono ) // query the usable buffer sizes ASIOGetBufferSize ( &HWBufferInfo.lMinSize, &HWBufferInfo.lMaxSize, &HWBufferInfo.lPreferredSize, &HWBufferInfo.lGranularity ); - //### TEST: BEGIN ###// + // ### TEST: BEGIN ###// /* #include QMessageBox::information ( 0, "APP_NAME", QString("lMinSize: %1, lMaxSize: %2, lPreferredSize: %3, lGranularity: %4"). @@ -319,12 +319,12 @@ int CSound::GetActualBufferSize ( const int iDesiredBufferSizeMono ) ); _exit(1); */ - //### TEST: END ###// + // ### TEST: END ###// - //### TODO: BEGIN ###// - // see https://github.com/EddieRingle/portaudio/blob/master/src/hostapi/asio/pa_asio.cpp#L1654 - // (SelectHostBufferSizeForUnspecifiedUserFramesPerBuffer) - //### TODO: END ###// + // ### TODO: BEGIN ###// + // see https://github.com/EddieRingle/portaudio/blob/master/src/hostapi/asio/pa_asio.cpp#L1654 + // (SelectHostBufferSizeForUnspecifiedUserFramesPerBuffer) + // ### TODO: END ###// // calculate "nearest" buffer size and set internal parameter accordingly // first check minimum and maximum values @@ -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 12caaf0aa3..7259be2e38 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 5fc793424c..910f0f220c 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 9ba70a01fd..96f2e397ce 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 e53dbece42..50f7324201 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,16 +178,66 @@ 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 */ ) { - //### TODO: BEGIN ###// - // try setting buffer size seems not to work! -> no audio after this operation! - // Doesn't this give an infinite loop? The set buffer size function will call our - // registered callback which calls "EmitReinitRequestSignal()". In that function - // this CSound::Init() function is called... - // jack_set_buffer_size ( pJackClient, iNewPrefMonoBufferSize ); - //### TODO: END ###// + // ### TODO: BEGIN ###// + // try setting buffer size seems not to work! -> no audio after this operation! + // Doesn't this give an infinite loop? The set buffer size function will call our + // registered callback which calls "EmitReinitRequestSignal()". In that function + // this CSound::Init() function is called... + // jack_set_buffer_size ( pJackClient, iNewPrefMonoBufferSize ); + // ### TODO: END ###// // without a Jack server, Jamulus makes no sense to run, throw an error message if ( bJackWasShutDown ) @@ -305,10 +341,10 @@ int CSound::process ( jack_nframes_t nframes, void* arg ) // copy packet and send it to the MIDI parser - //### TODO: BEGIN ###// - // do not call malloc in real-time callback + // ### TODO: BEGIN ###// + // do not call malloc in real-time callback CVector vMIDIPaketBytes ( in_event.size ); - //### TODO: END ###// + // ### TODO: END ###// for ( i = 0; i < static_cast ( in_event.size ); i++ ) { diff --git a/src/sound/jack/sound.h b/src/sound/jack/sound.h index 55b922ef7d..2008452135 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 654b05760d..0fbf27cfcf 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 3ce01f9ced..a87d5a5688 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 a5fbf188f8..ee84a516a4 100644 --- a/src/sound/soundbase.cpp +++ b/src/sound/soundbase.cpp @@ -237,6 +237,12 @@ QVector CSoundBase::LoadAndInitializeFirstValidDriver ( const bool bOpe \******************************************************************************/ 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,6 +373,7 @@ void CSoundBase::ParseMIDIMessage ( const CVector& vMIDIPaketBytes ) { const CMidiCtlEntry& cCtrl = aMidiCtls[vMIDIPaketBytes[1]]; const int iValue = vMIDIPaketBytes[2]; + emit MidiCCReceived ( vMIDIPaketBytes[1] ); ; switch ( cCtrl.eType ) { @@ -416,3 +423,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 13041143d8..1f054835ee 100644 --- a/src/sound/soundbase.h +++ b/src/sound/soundbase.h @@ -117,7 +117,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 +182,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 ); };