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..e915648f74 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -23,19 +23,21 @@ \******************************************************************************/ #include "client.h" +#include "settings.h" #include "util.h" /* Implementation *************************************************************/ CClient::CClient ( const quint16 iPortNumber, const quint16 iQosNumber, const QString& strConnOnStartupAddress, - const QString& strMIDISetup, const bool bNoAutoJackConnect, const QString& strNClientName, const bool bNEnableIPv6, const bool bNMuteMeInPersonalMix ) : ChannelInfo(), strClientName ( strNClientName ), + pSignalHandler ( CSignalHandler::getSingletonP() ), + pSettings ( nullptr ), Channel ( false ), /* we need a client channel -> "false" */ CurOpusEncoder ( nullptr ), CurOpusDecoder ( nullptr ), @@ -49,7 +51,7 @@ CClient::CClient ( const quint16 iPortNumber, bMuteOutStream ( false ), fMuteOutStreamGain ( 1.0f ), Socket ( &Channel, iPortNumber, iQosNumber, "", bNEnableIPv6 ), - Sound ( AudioCallback, this, strMIDISetup, bNoAutoJackConnect, strNClientName ), + Sound ( AudioCallback, this, bNoAutoJackConnect, strNClientName ), iAudioInFader ( AUD_FADER_IN_MIDDLE ), bReverbOnLeftChan ( false ), iReverbLevel ( 0 ), @@ -68,8 +70,7 @@ CClient::CClient ( const quint16 iPortNumber, bJitterBufferOK ( true ), bEnableIPv6 ( bNEnableIPv6 ), bMuteMeInPersonalMix ( bNMuteMeInPersonalMix ), - iServerSockBufNumFrames ( DEF_NET_BUF_SIZE_NUM_BL ), - pSignalHandler ( CSignalHandler::getSingletonP() ) + iServerSockBufNumFrames ( DEF_NET_BUF_SIZE_NUM_BL ) { int iOpusError; @@ -173,6 +174,8 @@ CClient::CClient ( const quint16 iPortNumber, QObject::connect ( pSignalHandler, &CSignalHandler::HandledSignal, this, &CClient::OnHandledSignal ); + QObject::connect ( &Sound, &CSoundBase::MidiCCReceived, this, [this] ( int ccNumber ) { emit MidiCCReceived ( ccNumber ); } ); + // start timer so that elapsed time works PreciseTime.start(); @@ -193,6 +196,33 @@ 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() +{ + // 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 ); + } +} + CClient::~CClient() { // if we were running, stop sound device @@ -1547,6 +1577,8 @@ void CClient::FreeClientChannel ( const int iServerChannelID ) */ } +void CClient::OnMidiCCReceived ( int ccNumber ) { emit MidiCCReceived ( ccNumber ); } + // find, and optionally create, a client channel for the supplied server channel ID // returns a client channel ID or INVALID_INDEX int CClient::FindClientChannel ( const int iServerChannelID, const bool bCreateIfNew ) diff --git a/src/client.h b/src/client.h index 8f266a371a..281e50ad4f 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, @@ -293,11 +292,23 @@ 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; +public: + void SetSettings ( CClientSettings* settings ); + 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 ); @@ -407,8 +418,6 @@ class CClient : public QObject int maxGainOrPanId; int iCurPingTime; - CSignalHandler* pSignalHandler; - protected slots: void OnHandledSignal ( int sigNum ); void OnSendProtMessage ( CVector vecMessage ); @@ -473,4 +482,8 @@ protected slots: void ControllerInFaderIsSolo ( int iChannelIdx, bool bIsSolo ); void ControllerInFaderIsMute ( int iChannelIdx, bool bIsMute ); void ControllerInMuteMyself ( bool bMute ); + void MidiCCReceived ( int ccNumber ); + +private slots: + void OnMidiCCReceived ( int ccNumber ); }; diff --git a/src/clientdlg.cpp b/src/clientdlg.cpp index cd678deacb..6063547896 100644 --- a/src/clientdlg.cpp +++ b/src/clientdlg.cpp @@ -29,7 +29,6 @@ CClientDlg::CClientDlg ( CClient* pNCliP, CClientSettings* pNSetP, const QString& strConnOnStartupAddress, - const QString& strMIDISetup, const bool bNewShowComplRegConnList, const bool bShowAnalyzerConsole, const bool bMuteStream, @@ -220,7 +219,7 @@ CClientDlg::CClientDlg ( CClient* pNCliP, MainMixerBoard->SetNumMixerPanelRows ( pSettings->iNumMixerPanelRows ); // Pass through flag for MIDICtrlUsed - MainMixerBoard->SetMIDICtrlUsed ( !strMIDISetup.isEmpty() ); + MainMixerBoard->SetMIDICtrlUsed ( pSettings->bUseMIDIController ); // reset mixer board MainMixerBoard->HideAll(); @@ -401,6 +400,12 @@ CClientDlg::CClientDlg ( CClient* pNCliP, pSettingsMenu->addAction ( tr ( "A&dvanced Settings..." ), this, SLOT ( OnOpenAdvancedSettings() ), QKeySequence ( Qt::CTRL + Qt::Key_D ) ); + pSettingsMenu->addAction ( + tr ( "&MIDI Control Settings..." ), + this, + [this] { ShowGeneralSettings ( SETTING_TAB_MIDI ); }, + QKeySequence ( Qt::CTRL + Qt::Key_M ) ); + // Main menu bar ----------------------------------------------------------- QMenuBar* pMenu = new QMenuBar ( this ); @@ -536,6 +541,8 @@ CClientDlg::CClientDlg ( CClient* pNCliP, QObject::connect ( &ClientSettingsDlg, &CClientSettingsDlg::NumMixerPanelRowsChanged, this, &CClientDlg::OnNumMixerPanelRowsChanged ); + QObject::connect ( &ClientSettingsDlg, &CClientSettingsDlg::MIDIControllerUsageChanged, this, &CClientDlg::OnMIDIControllerUsageChanged ); + QObject::connect ( this, &CClientDlg::SendTabChange, &ClientSettingsDlg, &CClientSettingsDlg::OnMakeTabChange ); QObject::connect ( MainMixerBoard, &CAudioMixerBoard::ChangeChanGain, this, &CClientDlg::OnChangeChanGain ); @@ -1526,3 +1533,13 @@ void CClientDlg::SetPingTime ( const int iPingTime, const int iOverallDelayMs, c // set current LED status ledDelay->SetLight ( eOverallDelayLEDColor ); } + +// OnOpenMidiSettings slot removed; lambda is used in menu action +void CClientDlg::OnMIDIControllerUsageChanged ( bool bEnabled ) +{ + // Update the mixer board's MIDI flag to trigger proper user numbering display + MainMixerBoard->SetMIDICtrlUsed ( bEnabled ); + + // Enable/disable runtime MIDI via the sound interface through the public CClient interface + pClient->EnableMIDI ( bEnabled ); +} diff --git a/src/clientdlg.h b/src/clientdlg.h index e26acb05f8..a5d327676a 100644 --- a/src/clientdlg.h +++ b/src/clientdlg.h @@ -80,7 +80,6 @@ class CClientDlg : public CBaseDlg, private Ui_CClientDlgBase CClientDlg ( CClient* pNCliP, CClientSettings* pNSetP, const QString& strConnOnStartupAddress, - const QString& strMIDISetup, const bool bNewShowComplRegConnList, const bool bShowAnalyzerConsole, const bool bMuteStream, @@ -246,6 +245,8 @@ public slots: void accept() { close(); } // introduced by pljones + void OnMIDIControllerUsageChanged ( bool bEnabled ); + signals: void SendTabChange ( int iTabIdx ); }; diff --git a/src/clientrpc.cpp b/src/clientrpc.cpp index f2c3f53cc3..832a85aa4e 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->iMidiChannel }, + { "midiMuteMyself", m_pSettings->iMidiMuteMyself }, + { "midiFaderOffset", m_pSettings->iMidiFaderOffset }, + { "midiFaderCount", m_pSettings->iMidiFaderCount }, + { "midiPanOffset", m_pSettings->iMidiPanOffset }, + { "midiPanCount", m_pSettings->iMidiPanCount }, + { "midiSoloOffset", m_pSettings->iMidiSoloOffset }, + { "midiSoloCount", m_pSettings->iMidiSoloCount }, + { "midiMuteOffset", m_pSettings->iMidiMuteOffset }, + { "midiMuteCount", m_pSettings->iMidiMuteCount } }; + response["result"] = jsonMidiParams; + Q_UNUSED ( params ); + } ); + + /// @rpc_method jamulusclient/setMidiSettings + /// @brief Sets one or more MIDI controller settings. + /// @param {object} params - Any subset of MIDI settings fields to set. + /// @result {string} result - Always "ok". + pRpcServer->HandleMethod ( "jamulusclient/setMidiSettings", [=] ( const QJsonObject& params, QJsonObject& response ) { + if ( params.contains ( "bUseMIDIController" ) ) + { + m_pSettings->bUseMIDIController = params["bUseMIDIController"].toBool(); + } + if ( params.contains ( "midiChannel" ) ) + { + m_pSettings->iMidiChannel = params["midiChannel"].toInt(); + } + if ( params.contains ( "midiMuteMyself" ) ) + { + m_pSettings->iMidiMuteMyself = params["midiMuteMyself"].toInt(); + } + if ( params.contains ( "midiFaderOffset" ) ) + { + m_pSettings->iMidiFaderOffset = params["midiFaderOffset"].toInt(); + } + if ( params.contains ( "midiFaderCount" ) ) + { + m_pSettings->iMidiFaderCount = params["midiFaderCount"].toInt(); + } + if ( params.contains ( "midiPanOffset" ) ) + { + m_pSettings->iMidiPanOffset = params["midiPanOffset"].toInt(); + } + if ( params.contains ( "midiPanCount" ) ) + { + m_pSettings->iMidiPanCount = params["midiPanCount"].toInt(); + } + if ( params.contains ( "midiSoloOffset" ) ) + { + m_pSettings->iMidiSoloOffset = params["midiSoloOffset"].toInt(); + } + if ( params.contains ( "midiSoloCount" ) ) + { + m_pSettings->iMidiSoloCount = params["midiSoloCount"].toInt(); + } + if ( params.contains ( "midiMuteOffset" ) ) + { + m_pSettings->iMidiMuteOffset = params["midiMuteOffset"].toInt(); + } + if ( params.contains ( "midiMuteCount" ) ) + { + m_pSettings->iMidiMuteCount = 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..479d5b31b8 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,58 @@ 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::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; + // Apply MIDI settings changes immediately + pClient->ApplyMidiSettingsFromConfig(); + } ); + } + + QObject::connect ( chbUseMIDIController, &QCheckBox::toggled, this, [this] ( bool checked ) { + pSettings->bUseMIDIController = checked; + SetMIDIControlsEnabled ( checked ); + + // Apply MIDI enable/disable immediately + pClient->ApplyMidiSettingsFromConfig(); + + 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 +845,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 +864,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->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() ); + + // Emit MIDIControllerUsageChanged signal to propagate MIDI state at startup + emit MIDIControllerUsageChanged ( chbUseMIDIController->isChecked() ); + + QDialog::showEvent ( event ); } void CClientSettingsDlg::UpdateJitterBufferFrame() @@ -1216,3 +1326,69 @@ void CClientSettingsDlg::OnAudioPanValueChanged ( int value ) pClient->SetAudioInFader ( value ); UpdateAudioFaderSlider(); } + +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..24429acff1 100644 --- a/src/clientsettingsdlg.h +++ b/src/clientsettingsdlg.h @@ -119,4 +119,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..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) @@ -267,6 +267,7 @@ LED bar: lbr #define SETTING_TAB_USER 0 #define SETTING_TAB_AUDIONET 1 #define SETTING_TAB_ADVANCED 2 +#define SETTING_TAB_MIDI 3 // common tool tip bottom line text #define TOOLTIP_COM_END_TEXT \ diff --git a/src/main.cpp b/src/main.cpp index 8e36f80e23..7739aa1f30 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -102,7 +102,6 @@ int main ( int argc, char** argv ) QString strJsonRpcBindIP = DEFAULT_JSON_RPC_LISTEN_ADDRESS; quint16 iQosNumber = DEFAULT_QOS_NUMBER; ELicenceType eLicenceType = LT_NO_LICENCE; - QString strMIDISetup = ""; QString strConnOnStartupAddress = ""; QString strIniFileName = ""; QString strHTMLStatusFileName = ""; @@ -535,7 +534,7 @@ int main ( int argc, char** argv ) continue; } - // Controller MIDI channel --------------------------------------------- + // MIDI if ( GetStringArgument ( argc, argv, i, @@ -543,9 +542,7 @@ int main ( int argc, char** argv ) "--ctrlmidich", strArgument ) ) { - strMIDISetup = strArgument; - qInfo() << qUtf8Printable ( QString ( "- MIDI controller settings: %1" ).arg ( strMIDISetup ) ); - CommandLineOptions << "--ctrlmidich"; + CommandLineOptions << QString ( "--ctrlmidich=%1" ).arg ( strArgument ); ClientOnlyOptions << "--ctrlmidich"; continue; } @@ -920,25 +917,17 @@ int main ( int argc, char** argv ) #ifndef SERVER_ONLY if ( bIsClient ) { - // Client: - // actual client object - CClient Client ( iPortNumber, - iQosNumber, - strConnOnStartupAddress, - strMIDISetup, - bNoAutoJackConnect, - strClientName, - bEnableIPv6, - bMuteMeInPersonalMix ); + CClient Client ( iPortNumber, iQosNumber, strConnOnStartupAddress, bNoAutoJackConnect, strClientName, bEnableIPv6, bMuteMeInPersonalMix ); - // load settings from init-file (command line options override) + // Create Settings with the client pointer CClientSettings Settings ( &Client, strIniFileName ); Settings.Load ( CommandLineOptions ); + Client.SetSettings ( &Settings ); # ifndef NO_JSON_RPC if ( pRpcServer ) { - new CClientRpc ( &Client, pRpcServer, pRpcServer ); + new CClientRpc ( &Client, &Settings, pRpcServer, pRpcServer ); } # endif @@ -956,7 +945,6 @@ int main ( int argc, char** argv ) CClientDlg ClientDlg ( &Client, &Settings, strConnOnStartupAddress, - strMIDISetup, bShowComplRegConnList, bShowAnalyzerConsole, bMuteStream, diff --git a/src/settings.cpp b/src/settings.cpp index 35c1f409a9..5de84851be 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -201,6 +201,125 @@ void CSettings::PutIniSetting ( QDomDocument& xmlFile, const QString& sSection, } #ifndef SERVER_ONLY + +// 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; +} + // Client settings ------------------------------------------------------------- void CClientSettings::LoadFaderSettings ( const QString& strCurFileName ) { @@ -226,7 +345,7 @@ void CClientSettings::SaveFaderSettings ( const QString& strCurFileName ) WriteToFile ( strCurFileName, IniXMLDocument ); } -void CClientSettings::ReadSettingsFromXML ( const QDomDocument& IniXMLDocument, const QList& ) +void CClientSettings::ReadSettingsFromXML ( const QDomDocument& IniXMLDocument, const QList& CommandLineOptions ) { int iIdx; int iValue; @@ -461,6 +580,59 @@ void CClientSettings::ReadSettingsFromXML ( const QDomDocument& IniXMLDocument, pClient->SetAudioQuality ( static_cast ( iValue ) ); } + // MIDI settings: check command line first, then fall back to XML + bool bMidiFromCommandLine = false; + 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 ); + bMidiFromCommandLine = true; + break; + } + } + + if ( !bMidiFromCommandLine ) + { + 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; + } + // custom directories //### TODO: BEGIN ###// @@ -548,7 +720,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 +728,6 @@ void CClientSettings::ReadSettingsFromXML ( const QDomDocument& IniXMLDocument, // fader settings ReadFaderSettingsFromXML ( IniXMLDocument ); } - void CClientSettings::ReadFaderSettingsFromXML ( const QDomDocument& IniXMLDocument ) { int iIdx; @@ -754,6 +925,19 @@ void CClientSettings::WriteSettingsToXML ( QDomDocument& IniXMLDocument, bool is // Settings Tab SetNumericIniSet ( IniXMLDocument, "client", "settingstab", iSettingsTab ); + // MIDI settings + SetNumericIniSet ( IniXMLDocument, "client", "midichannel", iMidiChannel ); + SetNumericIniSet ( IniXMLDocument, "client", "midifaderoffset", iMidiFaderOffset ); + SetNumericIniSet ( IniXMLDocument, "client", "midifadercount", iMidiFaderCount ); + SetNumericIniSet ( IniXMLDocument, "client", "midipanoffset", iMidiPanOffset ); + SetNumericIniSet ( IniXMLDocument, "client", "midipancount", iMidiPanCount ); + SetNumericIniSet ( IniXMLDocument, "client", "midisolooffset", iMidiSoloOffset ); + SetNumericIniSet ( IniXMLDocument, "client", "midisolocount", iMidiSoloCount ); + SetNumericIniSet ( IniXMLDocument, "client", "midimuteoffset", iMidiMuteOffset ); + SetNumericIniSet ( IniXMLDocument, "client", "midimutecount", iMidiMuteCount ); + SetNumericIniSet ( IniXMLDocument, "client", "midimutemyself", iMidiMuteMyself ); + SetFlagIniSet ( IniXMLDocument, "client", "usemidicontroller", bUseMIDIController ); + // fader settings WriteFaderSettingsToXML ( IniXMLDocument ); } diff --git a/src/settings.h b/src/settings.h index d87854d992..f324607511 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 ); @@ -201,6 +230,20 @@ class CClientSettings : public CSettings bool bWindowWasShownConnect; bool bOwnFaderFirst; + // MIDI settings + int iMidiChannel; + int iMidiMuteMyself; + int iMidiFaderOffset; + int iMidiFaderCount; + int iMidiPanOffset; + int iMidiPanCount; + int iMidiSoloOffset; + int iMidiSoloCount; + int iMidiMuteOffset; + int iMidiMuteCount; + bool bUseMIDIController; + QString strMidiDevice; + protected: virtual void WriteSettingsToXML ( QDomDocument& IniXMLDocument, bool isAboutToQuit ) override; virtual void ReadSettingsFromXML ( const QDomDocument& IniXMLDocument, const QList& ) override; diff --git a/src/sound/asio/sound.cpp b/src/sound/asio/sound.cpp index 40adacb1d2..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 ), @@ -629,6 +625,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..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(); @@ -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-ios/sound.h b/src/sound/coreaudio-ios/sound.h index af78c6a74e..475f759c7d 100644 --- a/src/sound/coreaudio-ios/sound.h +++ b/src/sound/coreaudio-ios/sound.h @@ -33,11 +33,7 @@ class CSound : public CSoundBase Q_OBJECT public: - CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), - void* arg, - const QString& strMIDISetup, - const bool, - const QString& ); + CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), void* arg, const bool, const QString& ); ~CSound(); virtual int Init ( const int iNewPrefMonoBufferSize ); diff --git a/src/sound/coreaudio-ios/sound.mm b/src/sound/coreaudio-ios/sound.mm index 807272b02e..33f47ee630 100644 --- a/src/sound/coreaudio-ios/sound.mm +++ b/src/sound/coreaudio-ios/sound.mm @@ -28,12 +28,8 @@ #define kInputBus 1 /* Implementation *************************************************************/ -CSound::CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), - void* arg, - const QString& strMIDISetup, - const bool, - const QString& ) : - CSoundBase ( "CoreAudio iOS", fpNewProcessCallback, arg, strMIDISetup ), +CSound::CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), void* arg, const bool, const QString& ) : + CSoundBase ( "CoreAudio iOS", fpNewProcessCallback, arg ), isInitialized ( false ) { try diff --git a/src/sound/coreaudio-mac/sound.cpp b/src/sound/coreaudio-mac/sound.cpp index 51e8775f67..28a9a647df 100644 --- a/src/sound/coreaudio-mac/sound.cpp +++ b/src/sound/coreaudio-mac/sound.cpp @@ -25,12 +25,9 @@ #include "sound.h" /* Implementation *************************************************************/ -CSound::CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), - void* arg, - const QString& strMIDISetup, - const bool, - const QString& ) : - CSoundBase ( "CoreAudio", fpNewProcessCallback, arg, strMIDISetup ), +CSound::CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), void* arg, const bool, const QString& ) : + CSoundBase ( "CoreAudio", fpNewProcessCallback, arg ), + midiClient ( static_cast ( NULL ) ), midiInPortRef ( static_cast ( NULL ) ) { // Apple Mailing Lists: Subject: GUI Apps should set kAudioHardwarePropertyRunLoop @@ -60,23 +57,22 @@ CSound::CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* iSelInputRightChannel = 0; iSelOutputLeftChannel = 0; iSelOutputRightChannel = 0; +} - // Optional MIDI initialization -------------------------------------------- - if ( iCtrlMIDIChannel != INVALID_MIDI_CH ) - { - // create client and ports - MIDIClientRef midiClient = static_cast ( NULL ); - MIDIClientCreate ( CFSTR ( APP_NAME ), NULL, NULL, &midiClient ); - MIDIInputPortCreate ( midiClient, CFSTR ( "Input port" ), callbackMIDI, this, &midiInPortRef ); - - // open connections from all sources - const int iNMIDISources = MIDIGetNumberOfSources(); +CSound::~CSound() +{ + // Ensure MIDI resources are properly cleaned up + DestroyMIDIPort(); // This will destroy the port if it exists - for ( int i = 0; i < iNMIDISources; i++ ) + // Explicitly destroy the client if it exists + if ( midiClient != static_cast ( NULL ) ) + { + OSStatus result = MIDIClientDispose ( midiClient ); + if ( result != noErr ) { - MIDIEndpointRef src = MIDIGetSource ( i ); - MIDIPortConnectSource ( midiInPortRef, src, NULL ); + qWarning() << "Failed to dispose CoreAudio MIDI client in destructor. Error code:" << result; } + midiClient = static_cast ( NULL ); } } @@ -718,6 +714,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..39d1aab3d1 100644 --- a/src/sound/coreaudio-mac/sound.h +++ b/src/sound/coreaudio-mac/sound.h @@ -38,11 +38,9 @@ class CSound : public CSoundBase Q_OBJECT public: - CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), - void* arg, - const QString& strMIDISetup, - const bool, - const QString& ); + CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), void* arg, const bool, const QString& ); + + virtual ~CSound(); virtual int Init ( const int iNewPrefMonoBufferSize ); virtual void Start(); @@ -63,6 +61,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 +110,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 +131,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..5971f92eea 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 a valid MIDI channel and no port exists + if ( input_port_midi == nullptr ) + { + CreateMIDIPort(); + } + } + else + { + // Destroy MIDI port if it exists + if ( input_port_midi != nullptr ) + { + DestroyMIDIPort(); + } + } +} + +bool CSound::IsMIDIEnabled() const { return ( input_port_midi != nullptr ); } + +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..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 ) @@ -87,6 +86,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 +106,8 @@ class CSound : public CSoundBase void OpenJack ( const bool bNoAutoJackConnect, const char* jackClientName ); void CloseJack(); + void CreateMIDIPort(); + void DestroyMIDIPort(); // callbacks static int process ( jack_nframes_t nframes, void* arg ); @@ -121,12 +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/midi-win/midi.cpp b/src/sound/midi-win/midi.cpp index 6823f6b6f3..1501e21ade 100644 --- a/src/sound/midi-win/midi.cpp +++ b/src/sound/midi-win/midi.cpp @@ -39,6 +39,11 @@ extern CSound* pSound; void CMidi::MidiStart() { + if ( m_bIsActive ) + { + return; // MIDI is already active, no need to start again + } + QString selMIDIDevice = pSound->GetMIDIDevice(); /* Get the number of MIDI In devices in this computer */ @@ -87,21 +92,36 @@ void CMidi::MidiStart() continue; // try next device, if any } - // success, add it to list of open handles + // Success, add it to the list of open handles vecMidiInHandles.append ( hMidiIn ); } + + if ( !vecMidiInHandles.isEmpty() ) + { + m_bIsActive = true; // Set active state if at least one device was started + } } void CMidi::MidiStop() { - // stop MIDI if running + if ( !m_bIsActive ) + { + return; // MIDI is already stopped, no need to stop again + } + + // Stop MIDI if running for ( int i = 0; i < vecMidiInHandles.size(); i++ ) { midiInStop ( vecMidiInHandles.at ( i ) ); midiInClose ( vecMidiInHandles.at ( i ) ); } + + vecMidiInHandles.clear(); // Clear the list of handles + m_bIsActive = false; // Set active state to false } +bool CMidi::IsActive() const { return m_bIsActive; } + // See https://learn.microsoft.com/en-us/previous-versions//dd798460(v=vs.85) // for the definition of the MIDI input callback function. void CALLBACK CMidi::MidiCallback ( HMIDIIN hMidiIn, UINT wMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2 ) diff --git a/src/sound/midi-win/midi.h b/src/sound/midi-win/midi.h index baa2f1286e..08154e0ab0 100644 --- a/src/sound/midi-win/midi.h +++ b/src/sound/midi-win/midi.h @@ -31,16 +31,18 @@ class CMidi { public: - CMidi() {} + CMidi() : m_bIsActive ( false ) {} virtual ~CMidi() {} void MidiStart(); void MidiStop(); + bool IsActive() const; protected: int iMidiDevs; QVector vecMidiInHandles; // windows handles + bool m_bIsActive; // Tracks if MIDI is currently active static void CALLBACK MidiCallback ( HMIDIIN hMidiIn, UINT wMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2 ); }; diff --git a/src/sound/oboe/sound.cpp b/src/sound/oboe/sound.cpp index 6886430bf2..c9e127077f 100644 --- a/src/sound/oboe/sound.cpp +++ b/src/sound/oboe/sound.cpp @@ -29,12 +29,8 @@ const uint8_t CSound::RING_FACTOR = 20; -CSound::CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), - void* arg, - const QString& strMIDISetup, - const bool, - const QString& ) : - CSoundBase ( "Oboe", fpNewProcessCallback, arg, strMIDISetup ) +CSound::CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), void* arg, const bool, const QString& ) : + CSoundBase ( "Oboe", fpNewProcessCallback, arg ) { #ifdef ANDROIDDEBUG qInstallMessageHandler ( myMessageHandler ); diff --git a/src/sound/oboe/sound.h b/src/sound/oboe/sound.h index e5b16c2daa..2c78fab08f 100644 --- a/src/sound/oboe/sound.h +++ b/src/sound/oboe/sound.h @@ -39,11 +39,7 @@ class CSound : public CSoundBase, public oboe::AudioStreamCallback public: static const uint8_t RING_FACTOR; - CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), - void* arg, - const QString& strMIDISetup, - const bool, - const QString& ); + CSound ( void ( *fpNewProcessCallback ) ( CVector& psData, void* arg ), void* arg, const bool, const QString& ); virtual ~CSound() {} virtual int Init ( const int iNewPrefMonoBufferSize ); diff --git a/src/sound/soundbase.cpp b/src/sound/soundbase.cpp index a1e077e076..f7c1ce0f4c 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 @@ -39,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 ), @@ -49,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 } @@ -235,102 +231,68 @@ QVector CSoundBase::LoadAndInitializeFirstValidDriver ( const bool bOpe /******************************************************************************\ * MIDI handling * \******************************************************************************/ -void CSoundBase::ParseCommandLineArgument ( const QString& strMIDISetup ) + +void CSoundBase::SetMIDIControllerMapping ( int iFaderOffset, + int iFaderCount, + int iPanOffset, + int iPanCount, + int iSoloOffset, + int iSoloCount, + int iMuteOffset, + int iMuteCount, + int iMuteMyselfCC ) { - int iMIDIOffsetFader = 70; // Behringer X-TOUCH: offset of 0x46 - - // parse the server info string according to definition: there is - // the legacy definition with just one or two numbers that only - // provides a definition for the controller offset of the level - // controllers (default 70 for the sake of Behringer X-Touch) - // [MIDI channel];[offset for level] - // - // The more verbose new form is a sequence of offsets for various - // controllers: at the current point, 'f', 'p', 's', and 'm' are - // parsed for fader, pan, solo, mute controllers respectively. - // However, at the current point of time only 'f' and 'p' - // controllers are actually implemented. The syntax for a Korg - // nanoKONTROL2 with 8 fader controllers starting at offset 0 and - // 8 pan controllers starting at offset 16 would be - // - // [MIDI channel];f0*8;p16*8 - // - // Namely a sequence of letters indicating the kind of controller, - // followed by the offset of the first such controller, followed - // by * and a count for number of controllers (if more than 1) - if ( !strMIDISetup.isEmpty() ) + // Clear all previous MIDI mappings + for ( int i = 0; i < aMidiCtls.size(); ++i ) { - // split the different parameter strings - const QStringList slMIDIParams = strMIDISetup.split ( ";" ); + aMidiCtls[i] = { None, 0 }; + } - // [MIDI channel] - if ( slMIDIParams.count() >= 1 ) + // Map fader controllers + for ( int i = 0; i < iFaderCount && i < MAX_NUM_CHANNELS; ++i ) + { + int iCC = iFaderOffset + i; + if ( iCC >= 0 && iCC < 128 ) { - iCtrlMIDIChannel = slMIDIParams[0].toUInt(); + aMidiCtls[iCC] = { Fader, i }; } + } - bool bSimple = true; // Indicates the legacy kind of specifying - // the fader controller offset without an - // indication of the count of controllers - - // [offset for level] - if ( slMIDIParams.count() >= 2 ) + // Map pan controllers + for ( int i = 0; i < iPanCount && i < MAX_NUM_CHANNELS; ++i ) + { + int iCC = iPanOffset + i; + if ( iCC >= 0 && iCC < 128 ) { - int i = slMIDIParams[1].toUInt ( &bSimple ); - // if the second parameter can be parsed as a number, we - // have the legacy specification of controllers. - if ( bSimple ) - iMIDIOffsetFader = i; + aMidiCtls[iCC] = { Pan, i }; } + } - if ( bSimple ) + // Map solo controllers + for ( int i = 0; i < iSoloCount && i < MAX_NUM_CHANNELS; ++i ) + { + int iCC = iSoloOffset + i; + if ( iCC >= 0 && iCC < 128 ) { - // For the legacy specification, we consider every controller - // up to the maximum number of channels (or the maximum - // controller number) a fader. - for ( int i = 0; i < MAX_NUM_CHANNELS; i++ ) - { - if ( i + iMIDIOffsetFader > 127 ) - break; - aMidiCtls[i + iMIDIOffsetFader] = { EMidiCtlType::Fader, i }; - } - return; + aMidiCtls[iCC] = { Solo, i }; } + } - // We have named controllers - - for ( int i = 1; i < slMIDIParams.count(); i++ ) + // Map mute controllers + for ( int i = 0; i < iMuteCount && i < MAX_NUM_CHANNELS; ++i ) + { + int iCC = iMuteOffset + i; + if ( iCC >= 0 && iCC < 128 ) { - QString sParm = slMIDIParams[i].trimmed(); - if ( sParm.isEmpty() ) - continue; - - int iCtrl = QString ( sMidiCtlChar ).indexOf ( sParm[0] ); - if ( iCtrl < 0 ) - continue; - EMidiCtlType eTyp = static_cast ( iCtrl ); - - if ( eTyp == Device ) - { - // save MIDI device name to select - strMIDIDevice = sParm.mid ( 1 ); - } - else - { - const QStringList slP = sParm.mid ( 1 ).split ( '*' ); - int iFirst = slP[0].toUInt(); - int iNum = ( slP.count() > 1 ) ? slP[1].toUInt() : 1; - for ( int iOff = 0; iOff < iNum; iOff++ ) - { - if ( iOff >= MAX_NUM_CHANNELS ) - break; - if ( iFirst + iOff >= 128 ) - break; - aMidiCtls[iFirst + iOff] = { eTyp, iOff }; - } - } + aMidiCtls[iCC] = { Mute, i }; } } + + // Map mute myself controller + if ( iMuteMyselfCC >= 0 && iMuteMyselfCC < 128 ) + { + aMidiCtls[iMuteMyselfCC] = { MuteMyself, 0 }; + } } void CSoundBase::ParseMIDIMessage ( const CVector& vMIDIPaketBytes ) @@ -367,7 +329,9 @@ void CSoundBase::ParseMIDIMessage ( const CVector& vMIDIPaketBytes ) { const CMidiCtlEntry& cCtrl = aMidiCtls[vMIDIPaketBytes[1]]; const int iValue = vMIDIPaketBytes[2]; - ; + + emit MidiCCReceived ( vMIDIPaketBytes[1] ); + switch ( cCtrl.eType ) { case Fader: diff --git a/src/sound/soundbase.h b/src/sound/soundbase.h index df08b679c0..2917684df9 100644 --- a/src/sound/soundbase.h +++ b/src/sound/soundbase.h @@ -62,15 +62,18 @@ class CMidiCtlEntry }; /* Classes ********************************************************************/ + +#ifndef SERVER_ONLY +class CClientSettings; +#endif + class CSoundBase : public QThread { Q_OBJECT - public: CSoundBase ( const QString& strNewSystemDriverTechniqueName, void ( *fpNewProcessCallback ) ( CVector& psData, void* pParg ), - void* pParg, - const QString& strMIDISetup ); + void* pParg ); virtual int Init ( const int iNewPrefMonoBufferSize ) { return iNewPrefMonoBufferSize; } virtual void Start() @@ -108,6 +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; } @@ -117,7 +121,21 @@ class CSoundBase : public QThread void EmitReinitRequestSignal ( const ESndCrdResetType eSndCrdResetType ) { emit ReinitRequest ( eSndCrdResetType ); } // this needs to be public so that it can be called from CMidi - void ParseMIDIMessage ( const CVector& vMIDIPaketBytes ); + void ParseMIDIMessage ( const CVector& vMIDIPaketBytes ); + virtual void EnableMIDI ( bool /* bEnable */ ) {} // Default empty implementation + virtual bool IsMIDIEnabled() const { return false; } // Default false + + void SetCtrlMIDIChannel ( int iCh ) { iCtrlMIDIChannel = iCh; } + + void SetMIDIControllerMapping ( int iFaderOffset, + int iFaderCount, + int iPanOffset, + int iPanCount, + int iSoloOffset, + int iSoloCount, + int iMuteOffset, + int iMuteCount, + int iMuteMyselfCC ); protected: virtual QString LoadAndInitializeDriver ( QString, bool ) { return ""; } @@ -179,4 +197,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 ); };