Skip to content

Commit

Permalink
Make "My Channel" work for all controls
Browse files Browse the repository at this point in the history
  • Loading branch information
pljones committed Oct 14, 2024
1 parent 14c669b commit 975e119
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 100 deletions.
35 changes: 19 additions & 16 deletions src/audiomixerboard.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1335,53 +1335,56 @@ void CAudioMixerBoard::ApplyNewConClientList ( CVector<CChannelInfo>& 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<size_t> ( iTheChannelIdx )]->IsVisible() )
if ( vecpChanFader[static_cast<size_t> ( iRealChannelIdx )]->IsVisible() )
{
vecpChanFader[static_cast<size_t> ( iTheChannelIdx )]->SetFaderLevel ( iValue );
vecpChanFader[static_cast<size_t> ( 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<size_t> ( iChannelIdx )]->IsVisible() )
if ( vecpChanFader[static_cast<size_t> ( iRealChannelIdx )]->IsVisible() )
{
vecpChanFader[static_cast<size_t> ( iChannelIdx )]->SetPanValue ( iValue );
vecpChanFader[static_cast<size_t> ( 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<size_t> ( iChannelIdx )]->IsVisible() )
if ( vecpChanFader[static_cast<size_t> ( iRealChannelIdx )]->IsVisible() )
{
vecpChanFader[static_cast<size_t> ( iChannelIdx )]->SetFaderIsSolo ( bIsSolo );
vecpChanFader[static_cast<size_t> ( 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<size_t> ( iChannelIdx )]->IsVisible() )
if ( vecpChanFader[static_cast<size_t> ( iRealChannelIdx )]->IsVisible() )
{
vecpChanFader[static_cast<size_t> ( iChannelIdx )]->SetFaderIsMute ( bIsMute );
vecpChanFader[static_cast<size_t> ( iRealChannelIdx )]->SetFaderIsMute ( bIsMute );
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/audiomixerboard.h
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,15 @@ class CAudioMixerBoard : public QGroupBox, public CAudioMixerBoardSlots<MAX_NUM_
template<unsigned int slotId>
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 );
Expand Down
210 changes: 128 additions & 82 deletions src/sound/soundbase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int16_t>& psData, void* pParg ),
Expand Down Expand Up @@ -233,98 +221,156 @@ QVector<QString> 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<EMidiCtlType> ( 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<EMidiCtlType> ( 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<EMidiCtlType> ( 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<EMidiCtlType> ( 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<uint8_t>& vMIDIPaketBytes )
{
if ( vMIDIPaketBytes.Size() > 0 )
Expand Down Expand Up @@ -357,22 +403,22 @@ void CSoundBase::ParseMIDIMessage ( const CVector<uint8_t>& 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<int> ( static_cast<double> ( 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:
Expand Down
16 changes: 14 additions & 2 deletions src/sound/soundbase.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ enum EMidiCtlType
Solo,
Mute,
MuteMyself,
OurFader, // Proposed addition: a MidiCtrlType for our own fader level
MyChannel,
None
};

Expand Down Expand Up @@ -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<CMidiCtlEntry> aMidiCtls;

Expand Down

0 comments on commit 975e119

Please sign in to comment.