diff --git a/ChangeLog b/ChangeLog index a36e6e6da2..557d4e4ef0 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,7 +1,8 @@ ### 3.7.0dev <- NOTE: the release version number will be 3.7.1 ### - +- Automatic channel fader adjustment simplifies mixer setup by using the channel level meters (#1071). + (contributed by @JohannesBrx) ### 3.7.0 (2021-03-17) ### diff --git a/src/audiomixerboard.cpp b/src/audiomixerboard.cpp index 616a24f025..531228c49d 100755 --- a/src/audiomixerboard.cpp +++ b/src/audiomixerboard.cpp @@ -857,6 +857,8 @@ CAudioMixerBoard::CAudioMixerBoard ( QWidget* parent ) : // create all mixer controls and make them invisible vecpChanFader.Init ( MAX_NUM_CHANNELS ); + vecAvgLevels.Init ( MAX_NUM_CHANNELS, 0.0f ); + for ( int i = 0; i < MAX_NUM_CHANNELS; i++ ) { vecpChanFader[i] = new CChannelFader ( this ); @@ -1145,6 +1147,7 @@ void CAudioMixerBoard::ApplyNewConClientList ( CVector& vecChanInf { // the fader was not in use, reset everything for new client vecpChanFader[i]->Reset(); + vecAvgLevels[i] = 0.0f; // check if this is my own fader and set fader property if ( i == iMyChannelID ) @@ -1308,6 +1311,169 @@ void CAudioMixerBoard::SetAllFaderLevelsToNewClientLevel() } } +void CAudioMixerBoard::AutoAdjustAllFaderLevels() +{ + QMutexLocker locker ( &Mutex ); + + // initialize variables used for statistics + float vecMaxLevel[MAX_NUM_FADER_GROUPS + 1]; + int vecChannelsPerGroup[MAX_NUM_FADER_GROUPS + 1]; + for ( int i = 0; i < MAX_NUM_FADER_GROUPS + 1; ++i ) + { + vecMaxLevel[i] = LOW_BOUND_SIG_METER; + vecChannelsPerGroup[i] = 0; + } + CVector> levels; + levels.resize ( MAX_NUM_FADER_GROUPS + 1 ); + + // compute min/max level per group and number of channels per group + for ( int i = 0; i < MAX_NUM_CHANNELS; ++i ) + { + // only apply to visible faders (and not to my own channel fader) + if ( vecpChanFader[i]->IsVisible() && ( i != iMyChannelID ) ) + { + // map averaged meter output level to decibels + // (invert CStereoSignalLevelMeter::CalcLogResultForMeter) + float leveldB = vecAvgLevels[i] * + ( UPPER_BOUND_SIG_METER - LOW_BOUND_SIG_METER ) / + NUM_STEPS_LED_BAR + LOW_BOUND_SIG_METER; + + int group = vecpChanFader[i]->GetGroupID(); + if ( group == INVALID_INDEX ) + { + group = MAX_NUM_FADER_GROUPS; + } + + if ( leveldB >= AUTO_FADER_NOISE_THRESHOLD_DB ) + { + vecMaxLevel[group] = fmax ( vecMaxLevel[group], leveldB ); + levels[group].Add ( leveldB ); + } + ++vecChannelsPerGroup[group]; + } + } + + // sort levels for later median computation + for ( int i = 0; i < MAX_NUM_FADER_GROUPS + 1; ++i ) + { + std::sort ( levels[i].begin(), levels[i].end() ); + } + + // compute the number of active groups (at least one channel) + int cntActiveGroups = 0; + for ( int i = 0; i < MAX_NUM_FADER_GROUPS; ++i ) + { + cntActiveGroups += vecChannelsPerGroup[i] > 0; + } + + // only my channel is active, nothing to do + if ( cntActiveGroups == 0 && + vecChannelsPerGroup[MAX_NUM_FADER_GROUPS] == 0 ) + { + return; + } + + // compute target level for each group + // (prevent clipping when each group contributes at maximum level) + float targetLevelPerGroup = -20.0f * log10 ( + std::max ( cntActiveGroups, 1 ) ); + + // compute target levels for the channels of each group individually + float vecTargetChannelLevel[MAX_NUM_FADER_GROUPS + 1]; + float levelOffset = 0.0f; + float minFader = 0.0f; + for ( int i = 0; i < MAX_NUM_FADER_GROUPS + 1; ++i ) + { + // compute the target level for each channel in the current group + // (prevent clipping when each channel in this group contributes at + // the maximum level) + vecTargetChannelLevel[i] = vecChannelsPerGroup[i] > 0 ? + targetLevelPerGroup - 20.0f * log10 ( vecChannelsPerGroup[i] ) : + 0.0f; + + // get median level + int cntChannels = levels[i].Size(); + if ( cntChannels == 0 ) + { + continue; + } + float refLevel = levels[i][cntChannels / 2]; + + // since we can only attenuate channels but not amplify, we have to + // check that the reference channel can be brought to the target + // level + if ( refLevel < vecTargetChannelLevel[i] ) + { + // otherwise, we adjust the level offset in such a way that + // the level can be reached + levelOffset = fmin ( levelOffset, + refLevel - vecTargetChannelLevel[i] ); + + // compute the minimum necessary fader setting + minFader = fmin ( minFader, -vecMaxLevel[i] + + vecTargetChannelLevel[i] + levelOffset ); + } + } + + // take minimum fader value into account + // very weak channels would actually require strong channels to be + // attenuated to a large amount; however, the attenuation is limited by + // the faders + if ( minFader < -AUD_MIX_FADER_RANGE_DB ) + { + levelOffset += -AUD_MIX_FADER_RANGE_DB - minFader; + } + + // adjust all levels + for ( int i = 0; i < MAX_NUM_CHANNELS; ++i ) + { + // only apply to visible faders (and not to my own channel fader) + if ( vecpChanFader[i]->IsVisible() && ( i != iMyChannelID ) ) + { + // map averaged meter output level to decibels + // (invert CStereoSignalLevelMeter::CalcLogResultForMeter) + float leveldB = vecAvgLevels[i] * + ( UPPER_BOUND_SIG_METER - LOW_BOUND_SIG_METER ) / + NUM_STEPS_LED_BAR + LOW_BOUND_SIG_METER; + + int group = vecpChanFader[i]->GetGroupID(); + if ( group == INVALID_INDEX ) + { + if ( cntActiveGroups > 0 ) + { + // do not adjust the channels without group in group mode + continue; + } + else + { + group = MAX_NUM_FADER_GROUPS; + } + } + + // do not adjust channels with almost zero level to full level since + // the channel might simply be silent at the moment + if ( leveldB >= AUTO_FADER_NOISE_THRESHOLD_DB ) + { + // compute new level + float newdBLevel = -leveldB + vecTargetChannelLevel[group] + + levelOffset; + + // map range from decibels to fader level + // (this inverts MathUtils::CalcFaderGain) + float newFaderLevel = ( newdBLevel / AUD_MIX_FADER_RANGE_DB + + 1.0f ) * AUD_MIX_FADER_MAX; + + // limit fader + newFaderLevel = fmin ( fmax ( newFaderLevel, 0.0f), + float ( AUD_MIX_FADER_MAX ) ); + + // set fader level + vecpChanFader[i]->SetFaderLevel ( newFaderLevel, true ); + } + } + } +} + void CAudioMixerBoard::StoreAllFaderSettings() { QMutexLocker locker ( &Mutex ); @@ -1511,6 +1677,11 @@ void CAudioMixerBoard::SetChannelLevels ( const CVector& vecChannelLev { if ( vecpChanFader[iChId]->IsVisible() && ( i < iNumChannelLevels ) ) { + // compute exponential moving average + vecAvgLevels[iChId] = + (1.0f - AUTO_FADER_ADJUST_ALPHA) * vecAvgLevels[iChId] + + AUTO_FADER_ADJUST_ALPHA * vecChannelLevel[i]; + vecpChanFader[iChId]->SetChannelLevel ( vecChannelLevel[i++] ); // show level only if we successfully received levels from the diff --git a/src/audiomixerboard.h b/src/audiomixerboard.h index 6a9ac0a347..3fd9168170 100755 --- a/src/audiomixerboard.h +++ b/src/audiomixerboard.h @@ -232,6 +232,7 @@ class CAudioMixerBoard : void SetAllFaderLevelsToNewClientLevel(); void StoreAllFaderSettings(); void LoadAllFaderSettings(); + void AutoAdjustAllFaderLevels(); protected: class CMixerBoardScrollArea : public QScrollArea @@ -276,6 +277,7 @@ class CAudioMixerBoard : ERecorderState eRecorderState; QMutex Mutex; EChSortType eChSortType; + CVector vecAvgLevels; virtual void UpdateGainValue ( const int iChannelIdx, const float fValue, diff --git a/src/clientdlg.cpp b/src/clientdlg.cpp index 7f58a969e6..993a343f72 100755 --- a/src/clientdlg.cpp +++ b/src/clientdlg.cpp @@ -340,6 +340,8 @@ CClientDlg::CClientDlg ( CClient* pNCliP, pEditMenu->addAction ( tr ( "Set All Faders to New Client &Level" ), this, SLOT ( OnSetAllFadersToNewClientLevel() ), QKeySequence ( Qt::CTRL + Qt::Key_L ) ); + pEditMenu->addAction ( tr ( "Auto-Adjust all &Faders" ), this, + SLOT ( OnAutoAdjustAllFaderLevels() ), QKeySequence ( Qt::CTRL + Qt::Key_F ) ); // Main menu bar ----------------------------------------------------------- QMenuBar* pMenu = new QMenuBar ( this ); diff --git a/src/clientdlg.h b/src/clientdlg.h index ee5d3da9b5..e3e1469287 100755 --- a/src/clientdlg.h +++ b/src/clientdlg.h @@ -177,6 +177,7 @@ public slots: void OnUseTowRowsForMixerPanel ( bool Checked ) { MainMixerBoard->SetNumMixerPanelRows ( Checked ? 2 : 1 ); } void OnClearAllStoredSoloMuteSettings(); void OnSetAllFadersToNewClientLevel() { MainMixerBoard->SetAllFaderLevelsToNewClientLevel(); } + void OnAutoAdjustAllFaderLevels() { MainMixerBoard->AutoAdjustAllFaderLevels(); } void OnSettingsStateChanged ( int value ); void OnChatStateChanged ( int value ); diff --git a/src/global.h b/src/global.h index aad0daefc2..28ff6444b6 100755 --- a/src/global.h +++ b/src/global.h @@ -152,6 +152,19 @@ LED bar: lbr #define AUD_MIX_FADER_MAX 100 #define AUD_MIX_PAN_MAX 100 +// range of audio mixer fader +#define AUD_MIX_FADER_RANGE_DB 35.0f + +// coefficient for averaging channel levels for automatic fader adjustment +#define AUTO_FADER_ADJUST_ALPHA 0.2f + +// target level for auto fader adjustment in decibels +#define AUTO_FADER_TARGET_LEVEL_DB -30.0f + +// threshold in decibels below which the channel is considered as noise +// and not adjusted +#define AUTO_FADER_NOISE_THRESHOLD_DB -40.0f + // maximum number of fader groups (must be consistent to audiomixerboard implementation) #define MAX_NUM_FADER_GROUPS 4 diff --git a/src/util.cpp b/src/util.cpp index 0202de2a25..fd922cf285 100755 --- a/src/util.cpp +++ b/src/util.cpp @@ -485,6 +485,7 @@ CAboutDlg::CAboutDlg ( QWidget* parent ) : CBaseDlg ( parent ) "

David Kastrup (dakhubgit)

" "

Jordan Lum (mulyaj)

" "

Noam Postavsky (npostavs)

" + "

Johannes Brauers (JohannesBrx)

" "
" + tr ( "For details on the contributions check out the " ) + "" + tr ( "Github Contributors list" ) + "." ); diff --git a/src/util.h b/src/util.h index fc7076c871..70a05b7e39 100755 --- a/src/util.h +++ b/src/util.h @@ -1281,7 +1281,8 @@ class MathUtils } else { - return powf ( 10.0f, ( fInValueRange0_1 * 35.0f - 35.0f ) / 20.0f ); + return powf ( 10.0f, ( fInValueRange0_1 - 1.0f ) * + AUD_MIX_FADER_RANGE_DB / 20.0f ); } } };