From 975e119414ec948eab84e96bb9a5de73ab400740 Mon Sep 17 00:00:00 2001 From: Peter L Jones Date: Fri, 4 Oct 2024 20:24:31 +0100 Subject: [PATCH] Make "My Channel" work for all controls --- src/audiomixerboard.cpp | 35 ++++--- src/audiomixerboard.h | 9 ++ src/sound/soundbase.cpp | 210 ++++++++++++++++++++++++---------------- src/sound/soundbase.h | 16 ++- 4 files changed, 170 insertions(+), 100 deletions(-) diff --git a/src/audiomixerboard.cpp b/src/audiomixerboard.cpp index d91416b61d..e640e90ddb 100644 --- a/src/audiomixerboard.cpp +++ b/src/audiomixerboard.cpp @@ -1335,53 +1335,56 @@ void CAudioMixerBoard::ApplyNewConClientList ( CVector& vecChanInf void CAudioMixerBoard::SetFaderLevel ( const int iChannelIdx, const int iValue ) { - // If iChannelIdx is I_MY_CHANNEL and our own channel ID is a valid index - // then we adjust our own fader level - const int iTheChannelIdx = ( iChannelIdx == I_MY_CHANNEL ) ? iMyChannelID : iChannelIdx; + const int iRealChannelIdx = getRealChannelIdx ( iChannelIdx ); // only apply new fader level if channel index is valid and the fader is visible - if ( ( iTheChannelIdx >= 0 ) && ( iTheChannelIdx < MAX_NUM_CHANNELS ) ) + if ( ( iRealChannelIdx >= 0 ) && ( iRealChannelIdx < MAX_NUM_CHANNELS ) ) { - if ( vecpChanFader[static_cast ( iTheChannelIdx )]->IsVisible() ) + if ( vecpChanFader[static_cast ( iRealChannelIdx )]->IsVisible() ) { - vecpChanFader[static_cast ( iTheChannelIdx )]->SetFaderLevel ( iValue ); + vecpChanFader[static_cast ( iRealChannelIdx )]->SetFaderLevel ( iValue ); } } } void CAudioMixerBoard::SetPanValue ( const int iChannelIdx, const int iValue ) { + const int iRealChannelIdx = getRealChannelIdx ( iChannelIdx ); + // only apply new pan value if channel index is valid and the panner is visible - if ( ( iChannelIdx >= 0 ) && ( iChannelIdx < MAX_NUM_CHANNELS ) && bDisplayPans ) + if ( ( iRealChannelIdx >= 0 ) && ( iRealChannelIdx < MAX_NUM_CHANNELS ) && bDisplayPans ) { - if ( vecpChanFader[static_cast ( iChannelIdx )]->IsVisible() ) + if ( vecpChanFader[static_cast ( iRealChannelIdx )]->IsVisible() ) { - vecpChanFader[static_cast ( iChannelIdx )]->SetPanValue ( iValue ); + vecpChanFader[static_cast ( iRealChannelIdx )]->SetPanValue ( iValue ); } } } void CAudioMixerBoard::SetFaderIsSolo ( const int iChannelIdx, const bool bIsSolo ) { - // only apply solo if channel index is valid and the fader is visible - if ( ( iChannelIdx >= 0 ) && ( iChannelIdx < MAX_NUM_CHANNELS ) ) + const int iRealChannelIdx = getRealChannelIdx ( iChannelIdx ); + // only apply solo if channel index is valid and the fader is visible + if ( ( iRealChannelIdx >= 0 ) && ( iRealChannelIdx < MAX_NUM_CHANNELS ) ) { - if ( vecpChanFader[static_cast ( iChannelIdx )]->IsVisible() ) + if ( vecpChanFader[static_cast ( iRealChannelIdx )]->IsVisible() ) { - vecpChanFader[static_cast ( iChannelIdx )]->SetFaderIsSolo ( bIsSolo ); + vecpChanFader[static_cast ( iRealChannelIdx )]->SetFaderIsSolo ( bIsSolo ); } } } void CAudioMixerBoard::SetFaderIsMute ( const int iChannelIdx, const bool bIsMute ) { + const int iRealChannelIdx = getRealChannelIdx ( iChannelIdx ); + // only apply mute if channel index is valid and the fader is visible - if ( ( iChannelIdx >= 0 ) && ( iChannelIdx < MAX_NUM_CHANNELS ) ) + if ( ( iRealChannelIdx >= 0 ) && ( iRealChannelIdx < MAX_NUM_CHANNELS ) ) { - if ( vecpChanFader[static_cast ( iChannelIdx )]->IsVisible() ) + if ( vecpChanFader[static_cast ( iRealChannelIdx )]->IsVisible() ) { - vecpChanFader[static_cast ( iChannelIdx )]->SetFaderIsMute ( bIsMute ); + vecpChanFader[static_cast ( iRealChannelIdx )]->SetFaderIsMute ( bIsMute ); } } } diff --git a/src/audiomixerboard.h b/src/audiomixerboard.h index ea4fbf59df..822ae8c1e5 100644 --- a/src/audiomixerboard.h +++ b/src/audiomixerboard.h @@ -287,6 +287,15 @@ class CAudioMixerBoard : public QGroupBox, public CAudioMixerBoardSlots inline void connectFaderSignalsToMixerBoardSlots(); + // When handling MIDI controllers for adjusting Jamulus channel controls, + // each channel is assigned by the server. As a user's Jamulus channel + // will vary on each server connection, to enable the assignment of MIDI + // controllers to the user's own Jamulus channel, the constant I_MY_CHANNEL + // is passed into the methods used to handle the requests. This method then + // maps I_MY_CHANNEL to the current value of the user's Jamulus channel, + // held in iMyChannel. + inline int getRealChannelIdx ( const int iChannelIdx ) const { return iChannelIdx == I_MY_CHANNEL ? iMyChannelID : iChannelIdx; } + signals: void ChangeChanGain ( int iId, float fGain, bool bIsMyOwnFader ); void ChangeChanPan ( int iId, float fPan ); diff --git a/src/sound/soundbase.cpp b/src/sound/soundbase.cpp index 56fff6a86d..94b3c01ab3 100644 --- a/src/sound/soundbase.cpp +++ b/src/sound/soundbase.cpp @@ -24,18 +24,6 @@ #include "soundbase.h" -// This is used as a lookup table for parsing option letters, mapping -// a single character to an EMidiCtlType -char const sMidiCtlChar[] = { - // Has to follow order of EMidiCtlType - /* [EMidiCtlType::Fader] = */ 'f', - /* [EMidiCtlType::Pan] = */ 'p', - /* [EMidiCtlType::Solo] = */ 's', - /* [EMidiCtlType::Mute] = */ 'm', - /* [EMidiCtlType::MuteMyself] = */ 'o', - /* [EMidiCtlType::OurFader] = */ 'z', // Proposed addition: a new enum value for "our fader" - /* [EMidiCtlType::None] = */ '\0' }; - /* Implementation *************************************************************/ CSoundBase::CSoundBase ( const QString& strNewSystemDriverTechniqueName, void ( *fpNewProcessCallback ) ( CVector& psData, void* pParg ), @@ -233,98 +221,156 @@ QVector CSoundBase::LoadAndInitializeFirstValidDriver ( const bool bOpe } /******************************************************************************\ -* MIDI handling * +* Command Line Handling * \******************************************************************************/ void CSoundBase::ParseCommandLineArgument ( const QString& strMIDISetup ) { - 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() ) + if ( strMIDISetup.isEmpty() ) { - // split the different parameter strings - const QStringList slMIDIParams = strMIDISetup.split ( ";" ); + // should be caught in main.cpp + return; + } + + // Parse the --ctrlmidich string. There are two formats. + // Default to the legacy kind of specifying the fader controller offset + // without an indication of the count of controllers. + bool bSimple = true; - // [MIDI channel] - if ( slMIDIParams.count() >= 1 ) + // split the different parameter strings + const QStringList slMIDIParams = strMIDISetup.split ( ";" ); + int iNumParams = slMIDIParams.count(); + + if ( iNumParams >= 1 ) + { + bool bChOK = false; + int i = slMIDIParams[0].toUInt ( &bChOK ); + if ( bChOK ) { - iCtrlMIDIChannel = slMIDIParams[0].toUInt(); + // [MIDI channel] supplied (else use default) + iCtrlMIDIChannel = i; } + else + { + // iCtrlMIDIChannel == INVALID_MIDI_CH, so no point continuing + return; + } + } - bool bSimple = true; // Indicates the legacy kind of specifying - // the fader controller offset without an - // indication of the count of controllers + // Use Behringer X-TOUCH as default offset of 0x46 + int iMIDIOffsetFader = 70; + if ( iNumParams >= 2 ) + { + // if there is a second parameter that can be parsed as a number, + // we have the legacy specification of controllers. + int i = slMIDIParams[1].toUInt ( &bSimple ); + if ( bSimple ) + { + // [offset for fader] supplied (else use default) + iMIDIOffsetFader = i; + } + } - // [offset for level] - if ( slMIDIParams.count() >= 2 ) + if ( bSimple ) + { + // 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 + iMIDIOffsetFader <= 127 && i < MAX_NUM_CHANNELS; i++ ) { - 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; + // add a list entry for the CMidiCtlEntry + aMidiCtls[i + iMIDIOffsetFader] = { EMidiCtlType::Fader, i }; } + return; + } - if ( bSimple ) + // We have named controllers + // Validate and see whether "MyChannel" option is present + + bool bMyChannel = false; + QStringList slValid; // keep track of valid entries to make later processing simple + + for ( int i = 0; i < iNumParams; i++ ) + { + QString sParm = slMIDIParams[i].trimmed(); + + if ( sParm.isEmpty() ) + { + // skip empty entries silently + continue; + } + + int iCtrl = sMidiCtl.indexOf ( sParm[0] ); + if ( iCtrl < 0 ) + { + // skip unknown entries silently + continue; + } + + if ( static_cast ( iCtrl ) == EMidiCtlType::MyChannel ) { - // 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++ ) + // once seen, just remember this + bMyChannel = true; + continue; + } + + const QStringList slP = sParm.mid ( 1 ).split ( '*' ); + + if ( slP.count() > 2 ) + { + // skip invalid entries silently + continue; + } + + bool bIsUInt = false; + + unsigned int u = slP[0].toUInt ( &bIsUInt ); + if ( !bIsUInt ) + { + // skip invalid entries silently + continue; + } + int iFirst = u; + + // silently default incoherent count to 1 + int iNum = 1; + if ( static_cast ( iCtrl ) != EMidiCtlType::MuteMyself && slP.count() == 2 ) + { + bIsUInt = false; + unsigned int u = slP[1].toUInt ( &bIsUInt ); + if ( bIsUInt ) { - if ( i + iMIDIOffsetFader > 127 ) - break; - aMidiCtls[i + iMIDIOffsetFader] = { EMidiCtlType::Fader, i }; + iNum = u; } - return; } - // We have named controllers + // store the valid entry in a more splittable format + slValid.append ( QString ( "%1*%2*%3" ).arg ( iCtrl ).arg ( iFirst ).arg ( iNum ) ); + } - for ( int i = 1; i < slMIDIParams.count(); i++ ) + foreach ( QString sParm, slValid ) + { + const QStringList slP = sParm.split ( '*' ); + const EMidiCtlType eTyp = static_cast ( slP[0].toInt() ); + const int iFirst = slP[1].toUInt(); + const int iNum = slP[2].toUInt(); + for ( int iOff = 0; iOff < iNum && iOff + iFirst <= 127 && iOff < MAX_NUM_CHANNELS; iOff++ ) { - 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 ); - - 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++ ) + // For MyChannel option, first offset is "MyChannel", then the rest are 0 to iNum-1 channels + if ( bMyChannel ) + { + aMidiCtls[iFirst + iOff] = { eTyp, iOff == 0 ? I_MY_CHANNEL : iOff - 1 }; + } + else { - if ( iOff >= MAX_NUM_CHANNELS ) - break; - if ( iFirst + iOff >= 128 ) - break; aMidiCtls[iFirst + iOff] = { eTyp, iOff }; } } } } +/******************************************************************************\ +* MIDI handling * +\******************************************************************************/ void CSoundBase::ParseMIDIMessage ( const CVector& vMIDIPaketBytes ) { if ( vMIDIPaketBytes.Size() > 0 ) @@ -357,22 +403,22 @@ void CSoundBase::ParseMIDIMessage ( const CVector& vMIDIPaketBytes ) // make sure packet is long enough if ( vMIDIPaketBytes.Size() > 2 && vMIDIPaketBytes[1] <= uint8_t ( 127 ) && vMIDIPaketBytes[2] <= uint8_t ( 127 ) ) { + // Where "MyChannel" is in effect, cCtrl.iChannel will be I_MY_CHANNEL + // for the first CC number in the range for a cCtrl.eType and then zero upwards. const CMidiCtlEntry& cCtrl = aMidiCtls[vMIDIPaketBytes[1]]; const int iValue = vMIDIPaketBytes[2]; - ; + switch ( cCtrl.eType ) { case Fader: - case OurFader: { // we are assuming that the controller number is the same // as the audio fader index and the range is 0-127 const int iFaderLevel = static_cast ( static_cast ( iValue ) / 127 * AUD_MIX_FADER_MAX ); - const int iTheChannel = cCtrl.eType == OurFader ? I_MY_CHANNEL : cCtrl.iChannel; // consider offset for the faders - emit ControllerInFaderLevel ( iTheChannel, iFaderLevel ); + emit ControllerInFaderLevel ( cCtrl.iChannel, iFaderLevel ); } break; case Pan: diff --git a/src/sound/soundbase.h b/src/sound/soundbase.h index ceacd1d429..0c3adb3b93 100644 --- a/src/sound/soundbase.h +++ b/src/sound/soundbase.h @@ -50,7 +50,7 @@ enum EMidiCtlType Solo, Mute, MuteMyself, - OurFader, // Proposed addition: a MidiCtrlType for our own fader level + MyChannel, None }; @@ -160,7 +160,19 @@ class CSoundBase : public QThread QMutex MutexAudioProcessCallback; QMutex MutexDevProperties; - QString strSystemDriverTechniqueName; + QString strSystemDriverTechniqueName; + + // This is used as a lookup table for parsing option letters, mapping + // a single character to an EMidiCtlType. Has to follow order of EMidiCtlType. + const QString sMidiCtl = QString ( "f" // [EMidiCtlType::Fader] + "p" // [EMidiCtlType::Pan] + "s" // [EMidiCtlType::Solo] + "m" // [EMidiCtlType::Mute] + "o" // [EMidiCtlType::MuteMyself] + "z" // [EMidiCtlType::MyChannel] + "\0" // [EMidiCtlType::None] + ); + int iCtrlMIDIChannel; QVector aMidiCtls;