From 18c8889ce4926c155a362aade564c557e45b58f8 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/sound/soundbase.cpp | 227 ++++++++++++++++++++++++++-------------- src/sound/soundbase.h | 16 ++- 2 files changed, 162 insertions(+), 81 deletions(-) diff --git a/src/sound/soundbase.cpp b/src/sound/soundbase.cpp index a51e9031c3..031c8fb0bf 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,176 @@ QVector CSoundBase::LoadAndInitializeFirstValidDriver ( const bool bOpe } /******************************************************************************\ -* MIDI handling * +* Command Line Handling * \******************************************************************************/ void CSoundBase::ParseCommandLineArgument ( const QString& strMIDISetup ) { + if ( strMIDISetup.isEmpty() ) + return; + 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 + // default to the legacy kind of specifying + // the fader controller offset without an + // indication of the count of controllers + bool bSimple = true; + + // Parse the --ctrlmidich string. There are two formats. + // + // The legacy definition has just one or two numbers that only + // provides a definition for the controller offset of the fader // controllers (default 70 for the sake of Behringer X-Touch) - // [MIDI channel];[offset for level] + // [MIDI channel];[offset for first fader] // - // 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 + // The more verbose new form is a sequence of offsets and counts for various + // controllers: + // [MIDI channel];[control letter][offset]*[count](;...) + // Currently, the following control letters are defined: + // Fader - 'f' + // Pan - 'p' + // Solo - 's' + // Mute - 'm' + // 'offset' is the base MIDI CC number for the control. + // 'count' is the number of CC values for the control (Jamulus channels). // + // Additionally, 'o' has a single offset and controls Mute Myself. + // + // In addition, 'z' reserves the first CC number for a control + // to mean "my Jamulus channel". (For example + // 1;f0*9;z + // would mean fader CC0 controlled "my" channel, with CC1 to CC8 for normal channels 1 to 8. + // + // An example 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() ) - { - // split the different parameter strings - const QStringList slMIDIParams = strMIDISetup.split ( ";" ); + // However, at the current point of time only 'f' and 'p' + // controllers are actually implemented. + + // split the different parameter strings + const QStringList slMIDIParams = strMIDISetup.split ( ";" ); - // [MIDI channel] - if ( slMIDIParams.count() >= 1 ) + int iMIDIParamsStart = 0; + if ( slMIDIParams.count() >= 1 ) + { + bool bChOK = false; + int i = slMIDIParams[0].toUInt ( &bChOK ); + if ( bChOK ) { - iCtrlMIDIChannel = slMIDIParams[0].toUInt(); + // [MIDI channel] supplied + iCtrlMIDIChannel = i; + iMIDIParamsStart = 1; } + else + { + // Something else, use default channel and try new-style parsing + bSimple = false; + } + } - bool bSimple = true; // Indicates the legacy kind of specifying - // the fader controller offset without an - // indication of the count of controllers + if ( bSimple && slMIDIParams.count() >= 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 hasMyChannel = false; + QStringList slValid; + + // if the first param was not a number, use "any channel" and see if it is a valid param + for ( int i = iMIDIParamsStart; i < slMIDIParams.count(); i++ ) + { + QString sParm = slMIDIParams[i].trimmed(); + + // skip empty entries silently + if ( sParm.isEmpty() ) + continue; + + // skip unknown entries silently + int iCtrl = sMidiCtl.indexOf ( sParm[0] ); + if ( iCtrl < 0 ) + continue; + + // once seen, just remember this + 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++ ) - { - if ( i + iMIDIOffsetFader > 127 ) - break; - aMidiCtls[i + iMIDIOffsetFader] = { EMidiCtlType::Fader, i }; - } - return; + hasMyChannel = true; + continue; } - // We have named controllers + const QStringList slP = sParm.mid ( 1 ).split ( '*' ); + + // skip invalid entries silently + if ( slP.count() > 2 ) + continue; + + bool bIsUInt = false; + + // skip invalid entries silently + unsigned int u = slP[0].toUInt( &bIsUInt ); + if ( !bIsUInt ) + continue; + int iFirst = u; + + // silently default incoherent count to 1 + int iNum = 1; + if ( slP.count() == 2 ) + { + bIsUInt = false; + unsigned int u = slP[0].toUInt( &bIsUInt ); + if ( bIsUInt ) + iNum = u; + } + 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[0].toUInt(); + const int iNum = slP[1].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 ( hasMyChannel ) + { + aMidiCtls[iFirst + iOff] = { eTyp, iOff == 0 ? INVALID_INDEX : 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 +423,23 @@ 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 ) ) { - const CMidiCtlEntry& cCtrl = aMidiCtls[vMIDIPaketBytes[1]]; - const int iValue = vMIDIPaketBytes[2]; - ; + // Where "MyChannel" is in effect, cCtrl.iChannel will be INVALID_INDEX + // 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 ? INVALID_INDEX : 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 fa7090b51a..9803fd7684 100644 --- a/src/sound/soundbase.h +++ b/src/sound/soundbase.h @@ -49,7 +49,7 @@ enum EMidiCtlType Solo, Mute, MuteMyself, - OurFader, // Proposed addition: a MidiCtrlType for our own fader level + MyChannel, None }; @@ -160,6 +160,20 @@ class CSoundBase : public QThread QMutex MutexDevProperties; QString strSystemDriverTechniqueName; + + // This is used as a lookup table for parsing option letters, mapping + // a single character to an EMidiCtlType + static char constexpr sMidiCtlChar[] = { + // Has to follow order of EMidiCtlType + /* [EMidiCtlType::Fader] = */ 'f', + /* [EMidiCtlType::Pan] = */ 'p', + /* [EMidiCtlType::Solo] = */ 's', + /* [EMidiCtlType::Mute] = */ 'm', + /* [EMidiCtlType::MuteMyself] = */ 'o', + /* [EMidiCtlType::MyFader] = */ 'z', + /* [EMidiCtlType::None] = */ '\0' }; + const QString sMidiCtl = QString ( sMidiCtlChar ); + int iCtrlMIDIChannel; QVector aMidiCtls;