Skip to content

Commit

Permalink
Merge pull request jamulussoftware#2535 from softins/gain-rate-limit
Browse files Browse the repository at this point in the history
Add rate-limiting for channel gain change messages
  • Loading branch information
softins authored Mar 23, 2022
2 parents 00c92ab + 68d5612 commit c72195f
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 0 deletions.
89 changes: 89 additions & 0 deletions src/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,11 @@ CClient::CClient ( const quint16 iPortNumber,
// start timer so that elapsed time works
PreciseTime.start();

// set gain delay timer to single-shot and connect handler function
TimerGain.setSingleShot ( true );

QObject::connect ( &TimerGain, &QTimer::timeout, this, &CClient::OnTimerRemoteChanGain );

// start the socket (it is important to start the socket after all
// initializations and connections)
Socket.Start();
Expand Down Expand Up @@ -299,6 +304,8 @@ void CClient::OnCLPingReceived ( CHostAddress InetAddr, int iMs )
const int iCurDiff = EvaluatePingMessage ( iMs );
if ( iCurDiff >= 0 )
{
iCurPingTime = iCurDiff; // store for use by gain message sending

emit PingTimeReceived ( iCurDiff );
}
}
Expand Down Expand Up @@ -335,15 +342,97 @@ void CClient::SetDoAutoSockBufSize ( const bool bValue )
CreateServerJitterBufferMessage();
}

// In order not to flood the server with gain change messages, particularly when using
// a MIDI controller, a timer is used to limit the rate at which such messages are generated.
// This avoids a potential long backlog of messages, since each must be ACKed before the next
// can be sent, and this ACK is subject to the latency of the server connection.
//
// When the first gain change message is requested after an idle period (i.e. the timer is not
// running), it will be sent immediately, and a 300ms timer started.
//
// If a gain change message is requested while the timer is still running, the new gain is not sent,
// but just stored in newGain[iId], and the minGainId and maxGainId updated to note the range of
// IDs that must be checked when the time expires (this will usually be a single channel
// unless channel grouping is being used). This avoids having to check all possible channels.
//
// When the timer fires, the channels minGainId <= iId < maxGainId are checked by comparing
// the last sent value in oldGain[iId] with any pending value in newGain[iId], and if they differ,
// the new value is sent, updating oldGain[iId] with the sent value. If any new values are
// sent, the timer is restarted so that further immediate updates will be pended.

void CClient::SetRemoteChanGain ( const int iId, const float fGain, const bool bIsMyOwnFader )
{
QMutexLocker locker ( &MutexGain );

// if this gain is for my own channel, apply the value for the Mute Myself function
if ( bIsMyOwnFader )
{
fMuteOutStreamGain = fGain;
}

if ( TimerGain.isActive() )
{
// just update the new value for sending later;
// will compare with oldGain[iId] when the timer fires
newGain[iId] = fGain;

// update range of channel IDs to check in the timer
if ( iId < minGainId )
minGainId = iId; // first value to check
if ( iId >= maxGainId )
maxGainId = iId + 1; // first value NOT to check

return;
}

// here the timer was not active:
// send the actual gain and reset the range of channel IDs to empty
oldGain[iId] = newGain[iId] = fGain;
Channel.SetRemoteChanGain ( iId, fGain );

StartDelayTimer();
}

void CClient::OnTimerRemoteChanGain()
{
QMutexLocker locker ( &MutexGain );
bool bSent = false;

for ( int iId = minGainId; iId < maxGainId; iId++ )
{
if ( newGain[iId] != oldGain[iId] )
{
// send new gain and record as old gain
float fGain = oldGain[iId] = newGain[iId];
Channel.SetRemoteChanGain ( iId, fGain );
bSent = true;
}
}

// if a new gain has been sent, reset the range of channel IDs to empty and start timer
if ( bSent )
{
StartDelayTimer();
}
}

// reset the range of channel IDs to check and start the delay timer
void CClient::StartDelayTimer()
{
maxGainId = 0;
minGainId = MAX_NUM_CHANNELS;

// start timer to delay sending further updates
// use longer delay when connected to server with higher ping time,
// double the ping time in order to allow a bit of overhead for other messages
if ( iCurPingTime < DEFAULT_GAIN_DELAY_PERIOD_MS / 2 )
{
TimerGain.start ( DEFAULT_GAIN_DELAY_PERIOD_MS );
}
else
{
TimerGain.start ( iCurPingTime * 2 );
}
}

bool CClient::SetServerAddr ( QString strNAddr )
Expand Down
15 changes: 15 additions & 0 deletions src/client.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@
// audio reverberation range
#define AUD_REVERB_MAX 100

// default delay period between successive gain updates (ms)
// this will be increased to double the ping time if connected to a distant server
#define DEFAULT_GAIN_DELAY_PERIOD_MS 50

// OPUS number of coded bytes per audio packet
// TODO we have to use new numbers for OPUS to avoid that old CELT packets
// are used in the OPUS decoder (which gives a bad noise output signal).
Expand Down Expand Up @@ -236,6 +240,8 @@ class CClient : public QObject
void SetMuteOutStream ( const bool bDoMute ) { bMuteOutStream = bDoMute; }

void SetRemoteChanGain ( const int iId, const float fGain, const bool bIsMyOwnFader );
void OnTimerRemoteChanGain();
void StartDelayTimer();

void SetRemoteChanPan ( const int iId, const float fPan ) { Channel.SetRemoteChanPan ( iId, fPan ); }

Expand Down Expand Up @@ -354,6 +360,15 @@ class CClient : public QObject
// for ping measurement
QElapsedTimer PreciseTime;

// for gain rate limiting
QMutex MutexGain;
QTimer TimerGain;
int minGainId;
int maxGainId;
float oldGain[MAX_NUM_CHANNELS];
float newGain[MAX_NUM_CHANNELS];
int iCurPingTime;

CSignalHandler* pSignalHandler;

protected slots:
Expand Down

0 comments on commit c72195f

Please sign in to comment.