From becbe61a3443839bf1723c0a29db68b9d66545c4 Mon Sep 17 00:00:00 2001 From: Hans Petter Selasky Date: Mon, 24 Aug 2020 20:09:59 +0200 Subject: [PATCH] Implement new RX jitter buffer for use with Jamulus. The new jitter buffer uses a timing histogram to compute the actual jitter relative to the audio device or system clock timer. This will give a more accurate jitter value. A sequence bit has been added to all transmitted frames to improve the clock drift computation in the analyzer console. Signed-off-by: Hans Petter Selasky --- Jamulus.pro | 6 +- src/analyzerconsole.cpp | 138 +++++++------- src/buffer.cpp | 297 ----------------------------- src/buffer.h | 225 +++++----------------- src/channel.cpp | 50 +++-- src/channel.h | 22 ++- src/client.cpp | 46 ++++- src/client.h | 29 ++- src/clientsettingsdlg.cpp | 2 +- src/jitterbuffer.cpp | 380 ++++++++++++++++++++++++++++++++++++++ src/jitterbuffer.h | 106 +++++++++++ src/protocol.cpp | 58 +++++- src/protocol.h | 9 + src/server.cpp | 44 ++++- src/server.h | 1 + 15 files changed, 811 insertions(+), 602 deletions(-) create mode 100644 src/jitterbuffer.cpp create mode 100644 src/jitterbuffer.h diff --git a/Jamulus.pro b/Jamulus.pro index 575a5e7d90..bc635e8117 100755 --- a/Jamulus.pro +++ b/Jamulus.pro @@ -367,7 +367,8 @@ HEADERS += src/buffer.h \ src/recorder/jamrecorder.h \ src/recorder/creaperproject.h \ src/recorder/cwavestream.h \ - src/signalhandler.h + src/signalhandler.h \ + src/jitterbuffer.h HEADERS_GUI = src/audiomixerboard.h \ src/chatdlg.h \ @@ -465,7 +466,8 @@ SOURCES += src/buffer.cpp \ src/util.cpp \ src/recorder/jamrecorder.cpp \ src/recorder/creaperproject.cpp \ - src/recorder/cwavestream.cpp + src/recorder/cwavestream.cpp \ + src/jitterbuffer.cpp SOURCES_GUI = src/audiomixerboard.cpp \ src/chatdlg.cpp \ diff --git a/src/analyzerconsole.cpp b/src/analyzerconsole.cpp index 38c79786a7..aaabd33aec 100644 --- a/src/analyzerconsole.cpp +++ b/src/analyzerconsole.cpp @@ -66,7 +66,7 @@ CAnalyzerConsole::CAnalyzerConsole ( CClient* pNCliP, pTabErrRateLayout->addWidget ( pGraphErrRate ); pMainTabWidget->addTab ( pTabWidgetBufErrRate, - tr ( "Error Rate of Each Buffer Size" ) ); + tr ( "Jitter statistics" ) ); // Connections ------------------------------------------------------------- @@ -121,103 +121,91 @@ void CAnalyzerConsole::DrawFrame() void CAnalyzerConsole::DrawErrorRateTrace() { + float fStats[JITTER_MAX_FRAME_COUNT]; + float fMax; + float fClockDrift; + float fPacketDrops; + float fPeerAdjust; + float fLocalAdjust; + int height = GraphGridFrame.height(); + int width = GraphGridFrame.width(); + int fsize = ( height + 31 ) / 32; + int xmax; + + // make room for text + height -= 2 * fsize; + // create painter QPainter GraphPainter ( &GraphImage ); - // get the network buffer error rates to be displayed - CVector vecButErrorRates; - double dLimit; - double dMaxUpLimit; - - pClient->GetBufErrorRates ( vecButErrorRates, dLimit, dMaxUpLimit ); - - // get the number of data elements - const int iNumBuffers = vecButErrorRates.Size(); - - // convert the limits in the log domain - const double dLogLimit = log10 ( dLimit ); - const double dLogMaxUpLimit = log10 ( dMaxUpLimit ); - - // use fixed y-axis scale where the limit line is in the middle of the graph - const double dMax = 0; - const double dMin = dLogLimit * 2; - - // calculate space between points on the x-axis - const double dXSpace = - static_cast ( GraphGridFrame.width() ) / ( iNumBuffers - 1 ); - - // plot the limit line as dashed line - const double dYValLimitInGraph = CalcYPosInGraph ( dMin, dMax, dLogLimit ); + // set font size + QFont font ( GraphPainter.font() ); + font.setPixelSize ( fsize ); + GraphPainter.setFont ( font ); - GraphPainter.setPen ( QPen ( QBrush ( LineLimitColor ), - iLineWidth, - Qt::DashLine ) ); + // set font pen + GraphPainter.setPen ( QPen ( QBrush ( GraphFrameColor ), 1 ) ); - GraphPainter.drawLine ( QPoint ( GraphGridFrame.x(), dYValLimitInGraph ), - QPoint ( GraphGridFrame.x() + - GraphGridFrame.width(), dYValLimitInGraph ) ); + // get jitter statistics + pClient->GetJitterStatistics ( fStats, fClockDrift, fPacketDrops, fPeerAdjust, fLocalAdjust ); - // plot the maximum upper limit line as a dashed line - const double dYValMaxUpLimitInGraph = CalcYPosInGraph ( dMin, dMax, dLogMaxUpLimit ); + // draw information about clock drift + GraphPainter.drawText ( QPoint ( GraphGridFrame.x(), GraphGridFrame.y() + fsize ), + QString("PeerClkDrift = %1 RxDrops = %2 PeerAdj = %3 LocalAdj = %4") + .arg ( fClockDrift, 0, 'f', 5 ) + .arg ( fPacketDrops, 0, 'f', 5 ) + .arg ( fPeerAdjust, 0, 'f', 5 ) + .arg ( fLocalAdjust, 0, 'f', 5 ) ); - GraphPainter.setPen ( QPen ( QBrush ( LineMaxUpLimitColor ), - iLineWidth, - Qt::DashLine ) ); - - GraphPainter.drawLine ( QPoint ( GraphGridFrame.x(), dYValMaxUpLimitInGraph ), - QPoint ( GraphGridFrame.x() + - GraphGridFrame.width(), dYValMaxUpLimitInGraph ) ); - - // plot the data - for ( int i = 0; i < iNumBuffers; i++ ) + for ( unsigned i = xmax = 0; i != JITTER_MAX_FRAME_COUNT; i++ ) { - // data convert in log domain - // check for special case if error rate is 0 (which would lead to -Inf - // after the log operation) - if ( vecButErrorRates[i] > 0 ) + if ( fStats[xmax] < fStats[i] ) { - vecButErrorRates[i] = log10 ( vecButErrorRates[i] ); + xmax = i; } - else + } + fMax = fStats[xmax]; + + if ( fMax > 1.0f ) + { + for ( unsigned i = 0; i != JITTER_MAX_FRAME_COUNT; i++ ) { - // definition: set it to lowest possible axis value - vecButErrorRates[i] = dMin; + fStats[i] /= fMax; } + } + else + { + memset(fStats, 0, sizeof(fStats)); + } + + float dX = (float) width / (float) JITTER_MAX_FRAME_COUNT; + QPoint lastPoint; + + // plot the first data + for ( unsigned i = 0; i != JITTER_MAX_FRAME_COUNT; i++ ) + { // calculate the actual point in the graph (in pixels) - const QPoint curPoint ( - GraphGridFrame.x() + static_cast ( dXSpace * i ), - CalcYPosInGraph ( dMin, dMax, vecButErrorRates[i] ) ); + const QPoint nextPoint ( + GraphGridFrame.x() + static_cast ( dX * i + dX / 2.0f ), + GraphGridFrame.y() + 2 * fsize + height - 1 - static_cast ( fStats[i] * ( height - 1 ) ) ); + + if (i == 0) + lastPoint = nextPoint; // draw a marker and a solid line which goes from the bottom to the // marker (similar to Matlab stem() function) - GraphPainter.setPen ( QPen ( QBrush ( LineColor ), + GraphPainter.setPen ( QPen ( QBrush ( LineColor ) , iMarkerSize, Qt::SolidLine, Qt::RoundCap ) ); - GraphPainter.drawPoint ( curPoint ); + GraphPainter.drawPoint ( nextPoint ); GraphPainter.setPen ( QPen ( QBrush ( LineColor ), iLineWidth ) ); - GraphPainter.drawLine ( QPoint ( curPoint.x(), - GraphGridFrame.y() + - GraphGridFrame.height() ), - curPoint ); - } -} - -int CAnalyzerConsole::CalcYPosInGraph ( const double dAxisMin, - const double dAxisMax, - const double dValue ) const -{ - // calculate value range - const double dValRange = dAxisMax - dAxisMin; - - // calculate current normalized y-axis value - const double dYValNorm = ( dValue - dAxisMin ) / dValRange; + GraphPainter.drawLine ( lastPoint, nextPoint ); - // consider the graph grid size to calculate the final y-axis value - return GraphGridFrame.y() + static_cast ( - static_cast ( GraphGridFrame.height() ) * ( 1 - dYValNorm ) ); + lastPoint = nextPoint; + } } diff --git a/src/buffer.cpp b/src/buffer.cpp index 70610e5ab5..4e91090c8f 100755 --- a/src/buffer.cpp +++ b/src/buffer.cpp @@ -87,300 +87,3 @@ bool CNetBuf::Get ( CVector& vecbyData, return bGetOK; } - - -/* Network buffer with statistic calculations implementation ******************/ -CNetBufWithStats::CNetBufWithStats() : - CNetBuf ( false ), // base class init: no simulation mode - iMaxStatisticCount ( MAX_STATISTIC_COUNT ), - bUseDoubleSystemFrameSize ( false ), - dAutoFilt_WightUpNormal ( IIR_WEIGTH_UP_NORMAL ), - dAutoFilt_WightDownNormal ( IIR_WEIGTH_DOWN_NORMAL ), - dAutoFilt_WightUpFast ( IIR_WEIGTH_UP_FAST ), - dAutoFilt_WightDownFast ( IIR_WEIGTH_DOWN_FAST ), - dErrorRateBound ( ERROR_RATE_BOUND ), - dUpMaxErrorBound ( UP_MAX_ERROR_BOUND ) -{ - // Define the sizes of the simulation buffers, - // must be NUM_STAT_SIMULATION_BUFFERS elements! - // Avoid the buffer length 1 because we do not have a solution for a - // sample rate offset correction. Caused by the jitter we usually get bad - // performance with just one buffer. - viBufSizesForSim[0] = 2; - viBufSizesForSim[1] = 3; - viBufSizesForSim[2] = 4; - viBufSizesForSim[3] = 5; - viBufSizesForSim[4] = 6; - viBufSizesForSim[5] = 7; - viBufSizesForSim[6] = 8; - viBufSizesForSim[7] = 9; - viBufSizesForSim[8] = 10; - viBufSizesForSim[9] = 11; - - // set all simulation buffers in simulation mode - for ( int i = 0; i < NUM_STAT_SIMULATION_BUFFERS; i++ ) - { - SimulationBuffer[i].SetIsSimulation ( true ); - } -} - -void CNetBufWithStats::GetErrorRates ( CVector& vecErrRates, - double& dLimit, - double& dMaxUpLimit ) -{ - // get all the averages of the error statistic - vecErrRates.Init ( NUM_STAT_SIMULATION_BUFFERS ); - - for ( int i = 0; i < NUM_STAT_SIMULATION_BUFFERS; i++ ) - { - vecErrRates[i] = ErrorRateStatistic[i].GetAverage(); - } - - // get the limits for the decisions - dLimit = dErrorRateBound; - dMaxUpLimit = dUpMaxErrorBound; -} - -void CNetBufWithStats::Init ( const int iNewBlockSize, - const int iNewNumBlocks, - const bool bPreserve ) -{ - // call base class Init - CNetBuf::Init ( iNewBlockSize, iNewNumBlocks, bPreserve ); - - // inits for statistics calculation - if ( !bPreserve ) - { - // set the auto filter weights and max statistic count - if ( bUseDoubleSystemFrameSize ) - { - dAutoFilt_WightUpNormal = IIR_WEIGTH_UP_NORMAL_DOUBLE_FRAME_SIZE; - dAutoFilt_WightDownNormal = IIR_WEIGTH_DOWN_NORMAL_DOUBLE_FRAME_SIZE; - dAutoFilt_WightUpFast = IIR_WEIGTH_UP_FAST_DOUBLE_FRAME_SIZE; - dAutoFilt_WightDownFast = IIR_WEIGTH_DOWN_FAST_DOUBLE_FRAME_SIZE; - iMaxStatisticCount = MAX_STATISTIC_COUNT_DOUBLE_FRAME_SIZE; - dErrorRateBound = ERROR_RATE_BOUND_DOUBLE_FRAME_SIZE; - dUpMaxErrorBound = UP_MAX_ERROR_BOUND_DOUBLE_FRAME_SIZE; - } - else - { - dAutoFilt_WightUpNormal = IIR_WEIGTH_UP_NORMAL; - dAutoFilt_WightDownNormal = IIR_WEIGTH_DOWN_NORMAL; - dAutoFilt_WightUpFast = IIR_WEIGTH_UP_FAST; - dAutoFilt_WightDownFast = IIR_WEIGTH_DOWN_FAST; - iMaxStatisticCount = MAX_STATISTIC_COUNT; - dErrorRateBound = ERROR_RATE_BOUND; - dUpMaxErrorBound = UP_MAX_ERROR_BOUND; - } - - for ( int i = 0; i < NUM_STAT_SIMULATION_BUFFERS; i++ ) - { - // init simulation buffers with the correct size - SimulationBuffer[i].Init ( iNewBlockSize, viBufSizesForSim[i] ); - - // init statistics - ErrorRateStatistic[i].Init ( iMaxStatisticCount, true ); - } - - // reset the initialization counter which controls the initialization - // phase length - ResetInitCounter(); - - // init auto buffer setting with a meaningful value, also init the - // IIR parameter with this value - iCurAutoBufferSizeSetting = 6; - dCurIIRFilterResult = iCurAutoBufferSizeSetting; - iCurDecidedResult = iCurAutoBufferSizeSetting; - } -} - -void CNetBufWithStats::ResetInitCounter() -{ - // start initialization phase of IIR filtering, use a quarter the size - // of the error rate statistic buffers which should be ok for a good - // initialization value (initialization phase should be as short as - // possible) - iInitCounter = iMaxStatisticCount / 4; -} - -bool CNetBufWithStats::Put ( const CVector& vecbyData, - const int iInSize ) -{ - // call base class Put - const bool bPutOK = CNetBuf::Put ( vecbyData, iInSize ); - - // update statistics calculations - for ( int i = 0; i < NUM_STAT_SIMULATION_BUFFERS; i++ ) - { - ErrorRateStatistic[i].Update ( - !SimulationBuffer[i].Put ( vecbyData, iInSize ) ); - } - - return bPutOK; -} - -bool CNetBufWithStats::Get ( CVector& vecbyData, - const int iOutSize ) -{ - // call base class Get - const bool bGetOK = CNetBuf::Get ( vecbyData, iOutSize ); - - // update statistics calculations - for ( int i = 0; i < NUM_STAT_SIMULATION_BUFFERS; i++ ) - { - ErrorRateStatistic[i].Update ( - !SimulationBuffer[i].Get ( vecbyData, iOutSize ) ); - } - - // update auto setting - UpdateAutoSetting(); - - return bGetOK; -} - -void CNetBufWithStats::UpdateAutoSetting() -{ - int iCurDecision = 0; // dummy initialization - int iCurMaxUpDecision = 0; // dummy initialization - bool bDecisionFound; - - - // Get regular error rate decision ----------------------------------------- - // Use a specified error bound to identify the best buffer size for the - // current network situation. Start with the smallest buffer and - // test for the error rate until the rate is below the bound. - bDecisionFound = false; - - for ( int i = 0; i < NUM_STAT_SIMULATION_BUFFERS - 1; i++ ) - { - if ( ( !bDecisionFound ) && - ( ErrorRateStatistic[i].GetAverage() <= dErrorRateBound ) ) - { - iCurDecision = viBufSizesForSim[i]; - bDecisionFound = true; - } - } - - if ( !bDecisionFound ) - { - // in case no buffer is below bound, use largest buffer size - iCurDecision = viBufSizesForSim[NUM_STAT_SIMULATION_BUFFERS - 1]; - } - - - // Get maximum upper error rate decision ----------------------------------- - // Use a specified error bound to identify the maximum upper error rate - // to identify if we have a too low buffer setting which gives a very - // bad performance constantly. Start with the smallest buffer and - // test for the error rate until the rate is below the bound. - bDecisionFound = false; - - for ( int i = 0; i < NUM_STAT_SIMULATION_BUFFERS - 1; i++ ) - { - if ( ( !bDecisionFound ) && - ( ErrorRateStatistic[i].GetAverage() <= dUpMaxErrorBound ) ) - { - iCurMaxUpDecision = viBufSizesForSim[i]; - bDecisionFound = true; - } - } - - if ( !bDecisionFound ) - { - // in case no buffer is below bound, use largest buffer size - iCurMaxUpDecision = viBufSizesForSim[NUM_STAT_SIMULATION_BUFFERS - 1]; - - // This is a worst case, something very bad had happened. Hopefully - // this was just temporary so that we initiate a new initialization - // phase to get quickly back to normal buffer sizes (hopefully). - ResetInitCounter(); - } - - - // Post calculation (filtering) -------------------------------------------- - // Define different weights for up and down direction. Up direction - // filtering shall be slower than for down direction since we assume - // that the lower value is the actual value which can be used for - // the current network condition. If the current error rate estimation - // is higher, it may be a temporary problem which should not change - // the current jitter buffer size significantly. - // For the initialization phase, use lower weight values to get faster - // adaptation. - double dWeightUp, dWeightDown; - const double dHysteresisValue = FILTER_DECISION_HYSTERESIS; - bool bUseFastAdaptation = false; - - // check for initialization phase - if ( iInitCounter > 0 ) - { - // decrease init counter - iInitCounter--; - - // use the fast adaptation - bUseFastAdaptation = true; - } - - // if the current detected buffer setting is below the maximum upper bound - // decision, then we enable a booster to go up to the minimum required - // number of buffer blocks (i.e. we use weights for fast adaptation) - if ( iCurAutoBufferSizeSetting < iCurMaxUpDecision ) - { - bUseFastAdaptation = true; - } - - if ( bUseFastAdaptation ) - { - dWeightUp = dAutoFilt_WightUpFast; - dWeightDown = dAutoFilt_WightDownFast; - } - else - { - dWeightUp = dAutoFilt_WightUpNormal; - dWeightDown = dAutoFilt_WightDownNormal; - } - - // apply non-linear IIR filter - MathUtils().UpDownIIR1 ( dCurIIRFilterResult, - static_cast ( iCurDecision ), - dWeightUp, - dWeightDown ); - -/* -// TEST store important detection parameters in file for debugging -static FILE* pFile = fopen ( "test.dat", "w" ); -static int icnt = 0; -if ( icnt == 50 ) -{ - fprintf ( pFile, "%d %e\n", iCurDecision, dCurIIRFilterResult ); - fflush ( pFile ); - icnt = 0; -} -else -{ - icnt++; -} -*/ - - // apply a hysteresis - iCurAutoBufferSizeSetting = - MathUtils().DecideWithHysteresis ( dCurIIRFilterResult, - iCurDecidedResult, - dHysteresisValue ); - - - // Initialization phase check and correction ------------------------------- - // sometimes in the very first period after a connection we get a bad error - // rate result -> delete this from the initialization phase - if ( iInitCounter == iMaxStatisticCount / 8 ) - { - // check error rate of the largest buffer as the indicator - if ( ErrorRateStatistic[NUM_STAT_SIMULATION_BUFFERS - 1]. - GetAverage() > dErrorRateBound ) - { - for ( int i = 0; i < NUM_STAT_SIMULATION_BUFFERS; i++ ) - { - ErrorRateStatistic[i].Reset(); - } - } - } -} diff --git a/src/buffer.h b/src/buffer.h index 3caeb18b9e..340577049a 100755 --- a/src/buffer.h +++ b/src/buffer.h @@ -27,66 +27,19 @@ #include "util.h" #include "global.h" - -/* Definitions ****************************************************************/ -// number of simulation network jitter buffers for evaluating the statistic -// NOTE If you want to change this number, the code has to modified, too! -#define NUM_STAT_SIMULATION_BUFFERS 10 - -// hysteresis for buffer size decision to avoid fast changes if close to the bound -#define FILTER_DECISION_HYSTERESIS 0.1 - -// definition of the upper error bound of the jitter buffers -#define ERROR_RATE_BOUND_DOUBLE_FRAME_SIZE 0.0005 -#define ERROR_RATE_BOUND ( ERROR_RATE_BOUND_DOUBLE_FRAME_SIZE / 2 ) - -// definition of the upper jitter buffer error bound, if that one is reached we -// have to speed up the filtering to quickly get out of a incorrect buffer -// size state -#define UP_MAX_ERROR_BOUND_DOUBLE_FRAME_SIZE 0.01 -#define UP_MAX_ERROR_BOUND ( UP_MAX_ERROR_BOUND_DOUBLE_FRAME_SIZE / 2 ) - -// each regular buffer access lead to a count for put and get, assuming 2.66 ms -// blocks we have 15 s / 2.66 ms * 2 = approx. 11000 -#define MAX_STATISTIC_COUNT_DOUBLE_FRAME_SIZE 11000 - -// each regular buffer access lead to a count for put and get, assuming 1.33 ms -// blocks we have 15 s / 1.33 ms * 2 = approx. 22500 -#define MAX_STATISTIC_COUNT 22500 - -// Note that the following definitions of the weigh constants assume a block -// size of 128 samples at a sampling rate of 48 kHz. -#define IIR_WEIGTH_UP_NORMAL_DOUBLE_FRAME_SIZE 0.999995 -#define IIR_WEIGTH_DOWN_NORMAL_DOUBLE_FRAME_SIZE 0.9999 -#define IIR_WEIGTH_UP_FAST_DOUBLE_FRAME_SIZE 0.9995 -#define IIR_WEIGTH_DOWN_FAST_DOUBLE_FRAME_SIZE 0.999 - -// convert numbers from 128 samples case using http://www.tsdconseil.fr/tutos/tuto-iir1-en.pdf -// and https://octave-online.net: -// gamma = exp(-Ts/tau), after some calculations we get: x=0.999995;exp(64/128*log(x)) -#define IIR_WEIGTH_UP_NORMAL 0.9999975 -#define IIR_WEIGTH_DOWN_NORMAL 0.99994999875 -#define IIR_WEIGTH_UP_FAST 0.9997499687422 -#define IIR_WEIGTH_DOWN_FAST 0.999499875 - - /* Classes ********************************************************************/ // Buffer base class ----------------------------------------------------------- template class CBufferBase { public: - CBufferBase ( const bool bNIsSim = false ) : - bIsSimulation ( bNIsSim ), bIsInitialized ( false ) {} - - void SetIsSimulation ( const bool bNIsSim ) { bIsSimulation = bNIsSim; } + CBufferBase () : + bIsInitialized ( false ) {} void Init ( const int iNewMemSize, const bool bPreserve = false ) { - // in simulation mode the size is not changed during operation -> we do - // not have to implement special code for this case // only enter the "preserve" branch, if object was already initialized - if ( bPreserve && ( !bIsSimulation ) && bIsInitialized ) + if ( bPreserve && bIsInitialized ) { // copy old data in new vector using get pointer as zero per // definition @@ -178,10 +131,7 @@ template class CBufferBase else { // allocate memory for actual data buffer - if ( !bIsSimulation ) - { - vecMemory.Init ( iNewMemSize ); - } + vecMemory.Init ( iNewMemSize ); // init buffer pointers and buffer state (empty buffer) iGetPos = 0; @@ -199,50 +149,36 @@ template class CBufferBase virtual bool Put ( const CVector& vecData, const int iInSize ) { - if ( bIsSimulation ) + // copy new data in internal buffer + int iCurPos = 0; + + if ( iPutPos + iInSize > iMemSize ) { - // in this simulation only the buffer pointers and the buffer state - // is updated, no actual data is transferred - iPutPos += iInSize; + // remaining space size for second block + const int iRemSpace = iPutPos + iInSize - iMemSize; - if ( iPutPos >= iMemSize ) + // data must be written in two steps because of wrap around + while ( iPutPos < iMemSize ) { - iPutPos -= iMemSize; + vecMemory[iPutPos++] = vecData[iCurPos++]; } - } - else - { - // copy new data in internal buffer - int iCurPos = 0; - if ( iPutPos + iInSize > iMemSize ) + for ( iPutPos = 0; iPutPos < iRemSpace; iPutPos++ ) { - // remaining space size for second block - const int iRemSpace = iPutPos + iInSize - iMemSize; - - // data must be written in two steps because of wrap around - while ( iPutPos < iMemSize ) - { - vecMemory[iPutPos++] = vecData[iCurPos++]; - } - - for ( iPutPos = 0; iPutPos < iRemSpace; iPutPos++ ) - { vecMemory[iPutPos] = vecData[iCurPos++]; - } - } - else - { - // data can be written in one step - std::copy ( vecData.begin(), - vecData.begin() + iInSize, - vecMemory.begin() + iPutPos ); - - // set the put position one block further (no wrap around needs - // to be considered here) - iPutPos += iInSize; } } + else + { + // data can be written in one step + std::copy ( vecData.begin(), + vecData.begin() + iInSize, + vecMemory.begin() + iPutPos ); + + // set the put position one block further (no wrap around needs + // to be considered here) + iPutPos += iInSize; + } // take care about wrap around of put pointer if ( iPutPos == iMemSize ) @@ -266,49 +202,35 @@ template class CBufferBase virtual bool Get ( CVector& vecData, const int iOutSize ) { - if ( bIsSimulation ) + // copy data from internal buffer in output buffer + int iCurPos = 0; + + if ( iGetPos + iOutSize > iMemSize ) { - // in this simulation only the buffer pointers and the buffer state - // is updated, no actual data is transferred - iGetPos += iOutSize; + // remaining data size for second block + const int iRemData = iGetPos + iOutSize - iMemSize; - if ( iGetPos >= iMemSize ) + // data must be read in two steps because of wrap around + while ( iGetPos < iMemSize ) { - iGetPos -= iMemSize; + vecData[iCurPos++] = vecMemory[iGetPos++]; + } + + for ( iGetPos = 0; iGetPos < iRemData; iGetPos++ ) + { + vecData[iCurPos++] = vecMemory[iGetPos]; } } else { - // copy data from internal buffer in output buffer - int iCurPos = 0; - - if ( iGetPos + iOutSize > iMemSize ) - { - // remaining data size for second block - const int iRemData = iGetPos + iOutSize - iMemSize; - - // data must be read in two steps because of wrap around - while ( iGetPos < iMemSize ) - { - vecData[iCurPos++] = vecMemory[iGetPos++]; - } + // data can be read in one step + std::copy ( vecMemory.begin() + iGetPos, + vecMemory.begin() + iGetPos + iOutSize, + vecData.begin() ); - for ( iGetPos = 0; iGetPos < iRemData; iGetPos++ ) - { - vecData[iCurPos++] = vecMemory[iGetPos]; - } - } - else - { - // data can be read in one step - std::copy ( vecMemory.begin() + iGetPos, - vecMemory.begin() + iGetPos + iOutSize, - vecData.begin() ); - - // set the get position one block further (no wrap around needs - // to be considered here) - iGetPos += iOutSize; - } + // set the get position one block further (no wrap around needs + // to be considered here) + iGetPos += iOutSize; } // take care about wrap around of get pointer @@ -378,10 +300,7 @@ template class CBufferBase virtual void Clear() { // clear memory - if ( !bIsSimulation ) - { - vecMemory.Reset ( 0 ); - } + vecMemory.Reset ( 0 ); // init buffer pointers and buffer state (empty buffer) iGetPos = 0; @@ -394,7 +313,6 @@ template class CBufferBase int iGetPos; int iPutPos; EBufState eBufState; - bool bIsSimulation; bool bIsInitialized; }; @@ -403,9 +321,6 @@ template class CBufferBase class CNetBuf : public CBufferBase { public: - CNetBuf ( const bool bNewIsSim = false ) : - CBufferBase ( bNewIsSim ) {} - void Init ( const int iNewBlockSize, const int iNewNumBlocks, const bool bPreserve = false ); @@ -420,52 +335,6 @@ class CNetBuf : public CBufferBase }; -// Network buffer (jitter buffer) with statistic calculations ------------------ -class CNetBufWithStats : public CNetBuf -{ -public: - CNetBufWithStats(); - - void Init ( const int iNewBlockSize, - const int iNewNumBlocks, - const bool bPreserve = false ); - - void SetUseDoubleSystemFrameSize ( const bool bNDSFSize ) { bUseDoubleSystemFrameSize = bNDSFSize; } - - virtual bool Put ( const CVector& vecbyData, const int iInSize ); - virtual bool Get ( CVector& vecbyData, const int iOutSize ); - - int GetAutoSetting() { return iCurAutoBufferSizeSetting; } - void GetErrorRates ( CVector& vecErrRates, - double& dLimit, - double& dMaxUpLimit ); - -protected: - void UpdateAutoSetting(); - void ResetInitCounter(); - - // statistic (do not use the vector class since the classes do not have - // appropriate copy constructor/operator) - CErrorRate ErrorRateStatistic[NUM_STAT_SIMULATION_BUFFERS]; - CNetBuf SimulationBuffer[NUM_STAT_SIMULATION_BUFFERS]; - int viBufSizesForSim[NUM_STAT_SIMULATION_BUFFERS]; - - double dCurIIRFilterResult; - int iCurDecidedResult; - int iInitCounter; - int iCurAutoBufferSizeSetting; - int iMaxStatisticCount; - - bool bUseDoubleSystemFrameSize; - double dAutoFilt_WightUpNormal; - double dAutoFilt_WightDownNormal; - double dAutoFilt_WightUpFast; - double dAutoFilt_WightDownFast; - double dErrorRateBound; - double dUpMaxErrorBound; -}; - - // Conversion buffer (very simple buffer) -------------------------------------- // For this very simple buffer no wrap around mechanism is implemented. We // assume here, that the applied buffers are an integer fraction of the total diff --git a/src/channel.cpp b/src/channel.cpp index 677ac492e7..ad71d89448 100755 --- a/src/channel.cpp +++ b/src/channel.cpp @@ -179,13 +179,7 @@ void CChannel::SetAudioStreamProperties ( const EAudComprType eNewAudComprType, iAudioFrameSizeSamples = SYSTEM_FRAME_SIZE_SAMPLES; } - MutexSocketBuf.lock(); - { - // init socket buffer - SockBuf.SetUseDoubleSystemFrameSize ( eAudioCompressionType == CT_OPUS ); // NOTE must be set BEFORE the init() - SockBuf.Init ( iNetwFrameSize, iCurSockBufNumFrames ); - } - MutexSocketBuf.unlock(); + SockBuf.Init ( iNetwFrameSize, bDoAutoSockBufSize ? 0 : iCurSockBufNumFrames ); MutexConvBuf.lock(); { @@ -203,8 +197,7 @@ void CChannel::SetAudioStreamProperties ( const EAudComprType eNewAudComprType, Protocol.CreateNetwTranspPropsMes ( NetworkTransportProps ); } -bool CChannel::SetSockBufNumFrames ( const int iNewNumFrames, - const bool bPreserve ) +bool CChannel::SetSockBufNumFrames ( const int iNewNumFrames ) { bool ReturnValue = true; // init with error bool bCurDoAutoSockBufSize = false; // we have to init but init values does not matter @@ -223,7 +216,7 @@ bool CChannel::SetSockBufNumFrames ( const int iNewNumFrames, // the network block size is a multiple of the minimum network // block size - SockBuf.Init ( iNetwFrameSize, iNewNumFrames, bPreserve ); + SockBuf.SetMaxInUse ( bDoAutoSockBufSize ? 0 : iNewNumFrames ); // store current auto socket buffer size setting in the mutex // region since if we use the current parameter below in the @@ -367,7 +360,7 @@ void CChannel::OnJittBufSizeChange ( int iNewJitBufSize ) { // manual setting is received, turn OFF auto setting and apply new value SetDoAutoSockBufSize ( false ); - SetSockBufNumFrames ( iNewJitBufSize, true ); + SetSockBufNumFrames ( iNewJitBufSize ); } } else @@ -447,14 +440,9 @@ void CChannel::OnNetTranspPropsReceived ( CNetworkTransportProps NetworkTranspor // is not larger than the allowed maximum value iFadeInCnt = std::min ( iFadeInCnt, iFadeInCntMax ); - MutexSocketBuf.lock(); - { - // update socket buffer (the network block size is a multiple of the - // minimum network frame size) - SockBuf.SetUseDoubleSystemFrameSize ( eAudioCompressionType == CT_OPUS ); // NOTE must be set BEFORE the init() - SockBuf.Init ( iNetwFrameSize, iCurSockBufNumFrames ); - } - MutexSocketBuf.unlock(); + // update socket buffer (the network block size is a multiple of the + // minimum network frame size) + SockBuf.Init ( iNetwFrameSize, bDoAutoSockBufSize ? 0 : iCurSockBufNumFrames ); MutexConvBuf.lock(); { @@ -539,16 +527,20 @@ EPutDataStat CChannel::PutAudioData ( const CVector& vecbyData, MutexSocketBuf.lock(); { // only process audio if packet has correct size - if ( iNumBytes == ( iNetwFrameSize * iNetwFrameSizeFact ) ) + if ( (iNumBytes % iNetwFrameSize) == 0 ) { - // store new packet in jitter buffer - if ( SockBuf.Put ( vecbyData, iNumBytes ) ) - { - eRet = PS_AUDIO_OK; - } - else + // set default return codee + eRet = PS_AUDIO_OK; + + for ( int i = 0; i != iNumBytes; i += iNetwFrameSize ) { - eRet = PS_AUDIO_ERR; + // store new packet in jitter buffer + if ( SockBuf.PutFrame ( vecbyData.data() + i, + iNetwFrameSize ) == false ) + { + eRet = PS_AUDIO_ERR; + break; + } } // manage audio fade-in counter @@ -606,7 +598,7 @@ EGetDataStat CChannel::GetData ( CVector& vecbyData, MutexSocketBuf.lock(); { // the socket access must be inside a mutex - const bool bSockBufState = SockBuf.Get ( vecbyData, iNumBytes ); + const bool bSockBufState = SockBuf.GetFrame ( vecbyData.data(), iNumBytes ); // decrease time-out counter if ( iConTimeOut > 0 ) @@ -710,6 +702,6 @@ void CChannel::UpdateSocketBufferSize() { // use auto setting result from channel, make sure we preserve the // buffer memory since we just adjust the size here - SetSockBufNumFrames ( SockBuf.GetAutoSetting(), true ); + SetSockBufNumFrames ( SockBuf.GetAutoSetting() ); } } diff --git a/src/channel.h b/src/channel.h index 530eb08981..8102a5cf9b 100755 --- a/src/channel.h +++ b/src/channel.h @@ -29,6 +29,7 @@ #include #include #include "global.h" +#include "jitterbuffer.h" #include "buffer.h" #include "util.h" #include "protocol.h" @@ -121,8 +122,7 @@ class CChannel : public QObject void SetRemoteChanPan ( const int iId, const float fPan ) { Protocol.CreateChanPanMes ( iId, fPan ); } - bool SetSockBufNumFrames ( const int iNewNumFrames, - const bool bPreserve = false ); + bool SetSockBufNumFrames ( const int iNewNumFrames ); int GetSockBufNumFrames() const { return iCurSockBufNumFrames; } void UpdateSocketBufferSize(); @@ -143,8 +143,20 @@ class CChannel : public QObject int GetNetwFrameSizeFact() const { return iNetwFrameSizeFact; } int GetNetwFrameSize() const { return iNetwFrameSize; } - void GetBufErrorRates ( CVector& vecErrRates, double& dLimit, double& dMaxUpLimit ) - { SockBuf.GetErrorRates ( vecErrRates, dLimit, dMaxUpLimit ); } + void GetJitterStatistics ( float *vecfStat, float &fClockDrift, float &fPacketDrops, + float &fPeerAdjust, float &fLocalAdjust ) + { + SockBuf.GetStatistics ( vecfStat ); + fClockDrift = SockBuf.GetClockDrift (); + fPacketDrops = SockBuf.GetPacketDrops (); + fPeerAdjust = SockBuf.GetPeerAdjust (); + fLocalAdjust = SockBuf.GetLocalAdjustStep (); + } + bool GetToggle () { return SockBuf.GetToggle(); } + void SetLocalAdjustment ( const float fAdjustment ) { return SockBuf.SetLocalAdjust ( fAdjustment ); } + float GetPeerAdjustment () { return SockBuf.GetPeerAdjust (); } + int GetLocalAdjustment () { return SockBuf.GetLocalAdjust (); } + bool IsLocalAdjustmentActive () { return SockBuf.IsAdjustActive (); } EAudComprType GetAudioCompressionType() { return eAudioCompressionType; } int GetNumAudioChannels() const { return iNumAudioChannels; } @@ -205,7 +217,7 @@ void CreateReqChannelLevelListMes() { Protocol.CreateReqChannelLevelListMes(); } CVector vecfPannings; // network jitter-buffer - CNetBufWithStats SockBuf; + CJitterBuffer SockBuf; int iCurSockBufNumFrames; bool bDoAutoSockBufSize; diff --git a/src/client.cpp b/src/client.cpp index 2858364458..ec66e2ad61 100755 --- a/src/client.cpp +++ b/src/client.cpp @@ -165,6 +165,9 @@ CClient::CClient ( const quint16 iPortNumber, QObject::connect ( &ConnLessProtocol, &CProtocol::CLPingReceived, this, &CClient::OnCLPingReceived ); + QObject::connect ( &ConnLessProtocol, &CProtocol::CLPingWithAdjustmentReceived, + this, &CClient::OnCLPingWithAdjustmentReceived ); + QObject::connect ( &ConnLessProtocol, &CProtocol::CLPingWithNumClientsReceived, this, &CClient::OnCLPingWithNumClientsReceived ); @@ -322,6 +325,25 @@ void CClient::OnCLPingReceived ( CHostAddress InetAddr, } } +void CClient::OnCLPingWithAdjustmentReceived ( CHostAddress InetAddr, + int iMs, + float fAdjustment ) +{ + // make sure we are running and the server address is correct + if ( IsRunning() && ( InetAddr == Channel.GetAddress() ) ) + { + // take care of wrap arounds (if wrapping, do not use result) + const int iCurDiff = EvaluatePingMessage ( iMs ); + if ( iCurDiff >= 0 ) + { + emit PingTimeReceived ( iCurDiff ); + + // update adjustment value - no need for locking + Channel.SetLocalAdjustment ( fAdjustment ); + } + } +} + void CClient::OnCLPingWithNumClientsReceived ( CHostAddress InetAddr, int iMs, int iNumClients ) @@ -1070,10 +1092,26 @@ void CClient::ProcessAudioDataIntern ( CVector& vecsStereoSndCrd ) } } - // send coded audio through the network - Channel.PrepAndSendPacket ( &Socket, - vecCeltData, - iCeltNumCodedBytes ); + // insert our toggle bit + vecCeltData[0] &= ~1; + vecCeltData[0] |= Channel.GetToggle(); + + switch ( Channel.GetLocalAdjustment () ) { + case 1: + // send coded audio through the network + Channel.PrepAndSendPacket ( &Socket, + vecCeltData, + iCeltNumCodedBytes ); + // FALLTHROUGH + case 0: + // send coded audio through the network + Channel.PrepAndSendPacket ( &Socket, + vecCeltData, + iCeltNumCodedBytes ); + // FALLTHROUGH + default: + break; + } } diff --git a/src/client.h b/src/client.h index b07539b37c..d1be9368ab 100755 --- a/src/client.h +++ b/src/client.h @@ -155,10 +155,9 @@ class CClient : public QObject void SetDoAutoSockBufSize ( const bool bValue ); bool GetDoAutoSockBufSize() const { return Channel.GetDoAutoSockBufSize(); } - void SetSockBufNumFrames ( const int iNumBlocks, - const bool bPreserve = false ) + void SetSockBufNumFrames ( const int iNumBlocks ) { - Channel.SetSockBufNumFrames ( iNumBlocks, bPreserve ); + Channel.SetSockBufNumFrames ( iNumBlocks ); } int GetSockBufNumFrames() { return Channel.GetSockBufNumFrames(); } @@ -251,7 +250,19 @@ class CClient : public QObject { Channel.CreateChatTextMes ( strChatText ); } void CreateCLPingMes() - { ConnLessProtocol.CreateCLPingMes ( Channel.GetAddress(), PreparePingMessage() ); } + { + if (Channel.IsLocalAdjustmentActive()) + { + // provide adjustment as part of ping message + ConnLessProtocol.CreateCLPingWithAdjustment ( Channel.GetAddress(), + PreparePingMessage(), + Channel.GetPeerAdjustment() ); + } + else + { + ConnLessProtocol.CreateCLPingMes ( Channel.GetAddress(), PreparePingMessage() ); + } + } void CreateCLServerListPingMes ( const CHostAddress& InetAddr ) { @@ -271,8 +282,10 @@ class CClient : public QObject int EstimatedOverallDelay ( const int iPingTimeMs ); - void GetBufErrorRates ( CVector& vecErrRates, double& dLimit, double& dMaxUpLimit ) - { Channel.GetBufErrorRates ( vecErrRates, dLimit, dMaxUpLimit ); } + void GetJitterStatistics ( float *vecfStats, float &fClockDrift, + float &fPacketDrops, float &fPeerAdjust, float &fLocalAdjust ) + { Channel.GetJitterStatistics ( vecfStats, fClockDrift, fPacketDrops, + fPeerAdjust, fLocalAdjust ); } // settings CChannelCoreInfo ChannelInfo; @@ -388,6 +401,10 @@ protected slots: void OnSendCLProtMessage ( CHostAddress InetAddr, CVector vecMessage ); + void OnCLPingWithAdjustmentReceived ( CHostAddress InetAddr, + int iMs, + float fAdjustment ); + void OnCLPingWithNumClientsReceived ( CHostAddress InetAddr, int iMs, int iNumClients ); diff --git a/src/clientsettingsdlg.cpp b/src/clientsettingsdlg.cpp index c8db53ceb6..363cb1eaae 100755 --- a/src/clientsettingsdlg.cpp +++ b/src/clientsettingsdlg.cpp @@ -569,7 +569,7 @@ void CClientSettingsDlg::OnDriverSetupClicked() void CClientSettingsDlg::OnNetBufValueChanged ( int value ) { - pClient->SetSockBufNumFrames ( value, true ); + pClient->SetSockBufNumFrames ( value ); UpdateJitterBufferFrame(); } diff --git a/src/jitterbuffer.cpp b/src/jitterbuffer.cpp new file mode 100644 index 0000000000..38f6191413 --- /dev/null +++ b/src/jitterbuffer.cpp @@ -0,0 +1,380 @@ +/******************************************************************************\ + * Copyright (c) 2020 + * + * Author(s): + * Hans Petter Selasky + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + * +\******************************************************************************/ + +#include + +#include "jitterbuffer.h" + +static uint32_t JitterSeqno[1U << JITTER_SEQNO_LOG2]; + +static void __attribute__((__constructor__)) +JitterSeqnoInit () +{ + uint32_t seq; + uint32_t r = 1; + uint32_t value = 0; + + for ( seq = 0; seq != JITTER_SEQNO_LOG2; seq++ ) + { + value *= 2; + if ( r & 1 ) + { + r += JITTER_SEQNO_PRIME; + value |= 1; + } + r /= 2; + } + + for ( seq = 0; seq != (JITTER_SEQNO_PRIME - 1); seq++ ) + { + JitterSeqno[value & JITTER_SEQNO_MASK] = seq; + + value *= 2; + if ( r & 1 ) + { + r += JITTER_SEQNO_PRIME; + value |= 1; + } + r /= 2; + } +} + +CSequenceNumber :: CSequenceNumber () +{ + TxRemainder = 1; + + InitToggle(); +} + +void +CSequenceNumber :: InitToggle () +{ + RxRemainder = 0; + RxLastSeqno = -1U; + RxValidSeqno = 0; + RxNextSeqno = 0; + RxTotalLocalSeqno = 0; + RxTotalPeerSeqno = 0; + RxTotalToggles = 0; + ClockDrift = 0.0f; + PacketDrops = 0.0f; + PeerAdjust = 0.0f; + LocalAdjustRemainder = 0.0f; + LocalAdjustStep = 0.0f; + AnyRxSeqnoReceived = false; +} + +CSequenceNumber :: ~CSequenceNumber () +{ +} + +void +CSequenceNumber :: RangeCheckCounters () +{ + static constexpr uint32_t limit = 100000; + + if ( RxTotalLocalSeqno >= limit || + RxTotalPeerSeqno >= limit || + RxTotalToggles >= limit ) + { + RxTotalLocalSeqno /= 2; + RxTotalPeerSeqno /= 2; + RxTotalToggles /= 2; + } +} + +void +CSequenceNumber :: PutToggle (const bool toggle) +{ + RxRemainder *= 2; + RxRemainder |= toggle; + + const uint32_t seq = JitterSeqno[RxRemainder & JITTER_SEQNO_MASK]; + + if ( seq == RxNextSeqno ) + { + // need to wait for a couple of numbers + // in sequence before we can trust the + // sequence number + if ( RxValidSeqno < JITTER_SEQNO_LOG2 ) + { + RxValidSeqno++; + } + else + { + if (RxLastSeqno != -1U) + { + // check for sequence number wraparound + if (seq < RxLastSeqno) + { + RxTotalPeerSeqno += seq + JITTER_SEQNO_PRIME - 1 - RxLastSeqno; + } + else + { + RxTotalPeerSeqno += seq - RxLastSeqno; + } + } + else + { + // first time init + RxTotalPeerSeqno = RxTotalLocalSeqno; + RxTotalToggles = RxTotalLocalSeqno; + } + + // update last sequence number + RxLastSeqno = seq; + // don't come here too often + RxValidSeqno = 1; + // record we received a valid sequence number + AnyRxSeqnoReceived = true; + + // compute some statistics + ClockDrift = (int32_t) (RxTotalLocalSeqno - RxTotalPeerSeqno) / + (float) RxTotalLocalSeqno; + PacketDrops = (int32_t) (RxTotalPeerSeqno - RxTotalToggles) / + (float) RxTotalToggles; + PeerAdjust = (int32_t) (RxTotalLocalSeqno - RxTotalToggles) / + (float) RxTotalPeerSeqno; + } + } + else + { + // out of sequence number detected + // reset valid counter + RxValidSeqno = 0; + } + + // compute next sequence number + RxNextSeqno = ( seq + 1 ) % ( JITTER_SEQNO_PRIME - 1 ); + // count number of calls + RxTotalToggles ++; + // range check the counters + RangeCheckCounters(); +} + +// this function produce the toggle value for +// the transmitted stream +bool +CSequenceNumber :: GetToggle () +{ + bool retval = ( TxRemainder & 1 ); + + if ( retval ) + TxRemainder += JITTER_SEQNO_PRIME; + TxRemainder /= 2; + + return (retval); +} + +int +CSequenceNumber :: GetLocalAdjust () +{ + LocalAdjustRemainder += LocalAdjustStep; + + if (LocalAdjustRemainder <= -1.0f) + { + // send less + LocalAdjustRemainder += 1.0f; + return (-1); + } + else if (LocalAdjustRemainder >= 1.0f) + { + // send more + LocalAdjustRemainder -= 1.0f; + return (1); + } + else + { + return (0); + } +} + +void +CSequenceNumber :: SetLocalAdjust (const float fValue) +{ + // limit the error rate so that we don't drop or add + // packets more often than a valid sequence number can + // be transmitted + LocalAdjustStep = qBound(-1.0f / ( 3 * JITTER_SEQNO_LOG2 ), + fValue, 1.0f / (3 * JITTER_SEQNO_LOG2) ); +} + +CJitterBuffer :: CJitterBuffer () +{ + RxCurInUse = 0; + RxMaxInUse = 0; + RxAutoInUse = 0; + RxCounter = 0; + RxSequence = 0; + RxOffset = 0; + TxOffset = 0; +} + +CJitterBuffer :: ~CJitterBuffer () +{ +} + +void +CJitterBuffer :: SetMaxInUse ( const int _RxMaxInUse ) +{ + QMutexLocker locker ( &RxMutex ); + RxMaxInUse = qBound ( 0, _RxMaxInUse, JITTER_MAX_FRAME_COUNT ); +} + +void +CJitterBuffer :: Init ( const size_t sMaxFrameSize, + const int _RxMaxInUse ) +{ + QMutexLocker locker ( &RxMutex ); + + for ( uint8_t x = 0; x != JITTER_MAX_FRAME_COUNT; x++ ) + { + vecvecbyData[x].Init ( sMaxFrameSize, 0 ); + } + + memset ( vecfHistogram, 0, sizeof ( vecfHistogram ) ); + + RxCurInUse = 0; + RxMaxInUse = qBound ( 0, _RxMaxInUse, JITTER_MAX_FRAME_COUNT ); + RxAutoInUse = JITTER_MAX_FRAME_COUNT; + RxCounter = 0; + RxSequence = 0; + RxOffset = 0; + TxOffset = 0; + + InitToggle (); +} + +bool +CJitterBuffer :: PutFrame ( const uint8_t *begin, const int len ) +{ + QMutexLocker locker ( &RxMutex ); + bool toggle = begin[0] & 1; + bool retval = false; + + // check if there is enough space in buffer + if ( RxCurInUse < ( RxMaxInUse ? RxMaxInUse : RxAutoInUse ) && + len == vecvecbyData[0].Size() ) + { + CVector &vecbyDst = vecvecbyData[RxOffset]; + + RxOffset++; + RxOffset %= JITTER_MAX_FRAME_COUNT; + + RxCurInUse++; + + memcpy ( vecbyDst.data(), begin, len ); + + retval = true; + } + + PutToggle(toggle); + + const uint32_t index = + ( JITTER_MAX_FRAME_COUNT + + RxCounter - + RxSequence ) % JITTER_MAX_FRAME_COUNT; + + RxCounter++; + RxCounter %= JITTER_MAX_FRAME_COUNT; + + // do the statistics + vecfHistogram[index] += 1.0f; + + // check if value reached the maximum + if (vecfHistogram[index] >= JITTER_MAX_FRAME_COUNT) + { + uint8_t x, xmax; + + for (x = 0; x != JITTER_MAX_FRAME_COUNT; x++) + { + vecfHistogram[x] /= 2.0f; + } + + // find width of peak + for (x = xmax = 1; x != (JITTER_MAX_FRAME_COUNT - 1) / 2; x++) + { + if (vecfHistogram[(index + x) % JITTER_MAX_FRAME_COUNT] >= 0.5f || + vecfHistogram[(JITTER_MAX_FRAME_COUNT + + index - x) % JITTER_MAX_FRAME_COUNT] >= 0.5f) + { + xmax = x + 1; + } + } + + // update current jitter setting + RxAutoInUse = 2 * xmax; + } + return retval; +} + +bool +CJitterBuffer :: GetFrame ( uint8_t *begin, const int len ) +{ + QMutexLocker locker ( &RxMutex ); + + // update RX sequence number (s) + RxSequence++; + RxSequence %= JITTER_MAX_FRAME_COUNT; + + // update local RX sequence number w/o wrapping + RxTotalLocalSeqno ++; + + // range check the counters + RangeCheckCounters(); + + // check if anything can be received + if ( RxCurInUse != 0 && + len == vecvecbyData[0].Size() ) + { + const CVector &vecbySrc = vecvecbyData[TxOffset]; + + TxOffset++; + TxOffset %= JITTER_MAX_FRAME_COUNT; + + RxCurInUse--; + + memcpy ( begin, vecbySrc.data(), len ); + + return true; + } + else + { + return false; + } +} + +void +CJitterBuffer :: GetStatistics ( float *pfStats ) +{ + memcpy ( pfStats, vecfHistogram, sizeof ( vecfHistogram ) ); +} + +int +CJitterBuffer :: GetAutoSetting () +{ + QMutexLocker locker ( &RxMutex ); + + return qBound ( MIN_NET_BUF_SIZE_NUM_BL, (int) RxAutoInUse, MAX_NET_BUF_SIZE_NUM_BL ); +} diff --git a/src/jitterbuffer.h b/src/jitterbuffer.h new file mode 100644 index 0000000000..8ddcd17c11 --- /dev/null +++ b/src/jitterbuffer.h @@ -0,0 +1,106 @@ +/******************************************************************************\ + * Copyright (c) 2020 + * + * Author(s): + * Hans Petter Selasky + * + ****************************************************************************** + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 2 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + * + * You should have received a copy of the GNU General Public License along with + * this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + * +\******************************************************************************/ + +#pragma once + +#include +#include + +#include "global.h" +#include "util.h" + +#define JITTER_SEQNO_PRIME 49139 // special prime number +#define JITTER_SEQNO_LOG2 16 // bits +#define JITTER_SEQNO_MASK ((1U << JITTER_SEQNO_LOG2) - 1U) + +class CSequenceNumber { +public: + CSequenceNumber (); + ~CSequenceNumber (); + + void InitToggle (); + void PutToggle ( const bool ); + bool GetToggle (); + float GetClockDrift () const { return ClockDrift; }; + float GetPacketDrops () const { return PacketDrops; }; + float GetPeerAdjust () const { return PeerAdjust; }; + float GetLocalAdjustStep () const { return LocalAdjustStep; }; + int GetLocalAdjust (); + void SetLocalAdjust ( const float ); + bool IsAdjustActive () { return AnyRxSeqnoReceived; }; + +protected: + void RangeCheckCounters (); + + uint32_t RxTotalLocalSeqno; + uint32_t RxTotalPeerSeqno; + uint32_t RxTotalToggles; + + uint32_t TxRemainder; + uint32_t RxRemainder; + uint32_t RxValidSeqno; + uint32_t RxNextSeqno; + uint32_t RxLastSeqno; + + // current remainder for adjustment + // in the range -1 .. +1 + float LocalAdjustRemainder; + + // cached and read only version of some numbers + float ClockDrift; + float PacketDrops; + float PeerAdjust; + float LocalAdjustStep; + + bool AnyRxSeqnoReceived; +}; + +#define JITTER_MAX_FRAME_COUNT 32 // preferably a power of two value + +class CJitterBuffer : public CSequenceNumber { +public: + CJitterBuffer (); + ~CJitterBuffer (); + + void Init ( const size_t sMaxFrameSize , const int RxMaxInUse ); + bool PutFrame ( const uint8_t *begin, const int len ); + bool GetFrame ( uint8_t *begin, const int len ); + void SetMaxInUse ( const int RxMaxInUse ); + void GetStatistics ( float *pfStats ); + int GetAutoSetting (); + +protected: + CVector vecvecbyData[JITTER_MAX_FRAME_COUNT]; + float vecfHistogram[JITTER_MAX_FRAME_COUNT]; + + QMutex RxMutex; + + uint32_t RxCurInUse; + uint32_t RxMaxInUse; + uint32_t RxAutoInUse; + uint32_t RxCounter; + uint32_t RxSequence; + uint32_t RxOffset; + uint32_t TxOffset; +}; diff --git a/src/protocol.cpp b/src/protocol.cpp index 1835351dd6..f637678ede 100755 --- a/src/protocol.cpp +++ b/src/protocol.cpp @@ -252,6 +252,12 @@ CONNECTION LESS MESSAGES | 4 bytes transmit time in ms | +-----------------------------+ +- PROTMESSID_CLM_PING_MS_WITHADJUSTMENT: Connection less ping message + + +-----------------------------+-----------------------------------+ + | 4 bytes transmit time in ms | 4 bytes TX rate adjustment factor | + +-----------------------------+-----------------------------------+ + - PROTMESSID_CLM_PING_MS_WITHNUMCLIENTS: Connection less ping message (for measuring the ping time) with the @@ -870,6 +876,10 @@ if ( rand() < ( RAND_MAX / 2 ) ) return false; EvaluateCLPingMes ( InetAddr, vecbyMesBodyData ); break; + case PROTMESSID_CLM_PING_MS_WITHADJUSTMENT: + EvaluateCLPingWithAdjustment ( InetAddr, vecbyMesBodyData ); + break; + case PROTMESSID_CLM_PING_MS_WITHNUMCLIENTS: EvaluateCLPingWithNumClientsMes ( InetAddr, vecbyMesBodyData ); break; @@ -1763,7 +1773,6 @@ bool CProtocol::EvaluateRecorderStateMes(const CVector& vecData) return false; // no error } - // Connection less messages ---------------------------------------------------- void CProtocol::CreateCLPingMes ( const CHostAddress& InetAddr, const int iMs ) { @@ -1798,6 +1807,53 @@ bool CProtocol::EvaluateCLPingMes ( const CHostAddress& InetAddr, return false; // no error } +// Connection less messages ---------------------------------------------------- +void CProtocol::CreateCLPingWithAdjustment ( const CHostAddress& InetAddr, + const int iMs, const float fAdjust ) +{ + static constexpr float fAdjustFactor = (1LL << 31) - 1LL; + + int iPos = 0; // init position pointer + + // build data vector (8 bytes long) + CVector vecData ( 8 ); + + // transmit time (4 bytes) + PutValOnStream ( vecData, iPos, static_cast ( iMs ), 4 ); + + // transmit adjustment (4 bytes) + PutValOnStream ( vecData, iPos, static_cast ( fAdjust * fAdjustFactor), 4 ); + + CreateAndImmSendConLessMessage ( PROTMESSID_CLM_PING_MS_WITHADJUSTMENT, + vecData, + InetAddr ); +} + +bool CProtocol::EvaluateCLPingWithAdjustment ( const CHostAddress& InetAddr, + const CVector& vecData ) +{ + static constexpr float fAdjustFactor = (1LL << 31) - 1LL; + + int iPos = 0; // init position pointer + + // check size + if ( vecData.Size() != 8 ) + { + return true; // return error code + } + + // transmit time + const int iCurMs = static_cast ( GetValFromStream ( vecData, iPos, 4 ) ); + + // get adjustment factor + const float fAdjust = static_cast ( GetValFromStream ( vecData, iPos, 4 ) ) / fAdjustFactor; + + // invoke message action + emit CLPingWithAdjustmentReceived ( InetAddr, iCurMs, fAdjust ); + + return false; // no error +} + void CProtocol::CreateCLPingWithNumClientsMes ( const CHostAddress& InetAddr, const int iMs, const int iNumClients ) diff --git a/src/protocol.h b/src/protocol.h index cb0bbd2bce..2fe87909f4 100755 --- a/src/protocol.h +++ b/src/protocol.h @@ -84,6 +84,7 @@ #define PROTMESSID_CLM_REGISTER_SERVER_RESP 1016 // status of server registration request #define PROTMESSID_CLM_REGISTER_SERVER_EX 1017 // register server with extended information #define PROTMESSID_CLM_RED_SERVER_LIST 1018 // reduced server list +#define PROTMESSID_CLM_PING_MS_WITHADJUSTMENT 1019 // for measuring ping time // special IDs #define PROTMESSID_SPECIAL_SPLIT_MESSAGE 2001 // a container for split messages @@ -136,6 +137,9 @@ void CreateReqChannelLevelListMes(); void CreateRecorderStateMes ( const ERecorderState eRecorderState ); void CreateCLPingMes ( const CHostAddress& InetAddr, const int iMs ); + void CreateCLPingWithAdjustment ( const CHostAddress& InetAddr, + const int iMs, + const float fAdjustment ); void CreateCLPingWithNumClientsMes ( const CHostAddress& InetAddr, const int iMs, const int iNumClients ); @@ -287,6 +291,8 @@ void CreateReqChannelLevelListMes(); bool EvaluateCLPingMes ( const CHostAddress& InetAddr, const CVector& vecData ); + bool EvaluateCLPingWithAdjustment ( const CHostAddress& InetAddr, + const CVector& vecData ); bool EvaluateCLPingWithNumClientsMes ( const CHostAddress& InetAddr, const CVector& vecData ); bool EvaluateCLServerFullMes(); @@ -361,6 +367,9 @@ public slots: void CLPingReceived ( CHostAddress InetAddr, int iMs ); + void CLPingWithAdjustmentReceived ( CHostAddress InetAddr, + int iMs, + float fAdjustment ); void CLPingWithNumClientsReceived ( CHostAddress InetAddr, int iMs, int iNumClients ); diff --git a/src/server.cpp b/src/server.cpp index 71144285e3..580989eb62 100755 --- a/src/server.cpp +++ b/src/server.cpp @@ -429,6 +429,9 @@ CServer::CServer ( const int iNewMaxNumChan, QObject::connect ( &ConnLessProtocol, &CProtocol::CLPingReceived, this, &CServer::OnCLPingReceived ); + QObject::connect ( &ConnLessProtocol, &CProtocol::CLPingWithAdjustmentReceived, + this, &CServer::OnCLPingWithAdjustmentReceived ); + QObject::connect ( &ConnLessProtocol, &CProtocol::CLPingWithNumClientsReceived, this, &CServer::OnCLPingWithNumClientsReceived ); @@ -1320,10 +1323,27 @@ opus_custom_encoder_ctl ( pCurOpusEncoder, OPUS_SET_BITRATE ( CalcBitRateBitsPer iCeltNumCodedBytes ); } - // send separate mix to current clients - vecChannels[iCurChanID].PrepAndSendPacket ( &Socket, - vecvecbyCodedData[iChanCnt], - iCeltNumCodedBytes ); + // insert our toggle bit + vecvecbyCodedData[iChanCnt][0] &= ~1; + vecvecbyCodedData[iChanCnt][0] |= + vecChannels[iCurChanID].GetToggle(); + + switch ( vecChannels[iCurChanID].GetLocalAdjustment () ) { + case 1: + // send separate mix to current clients + vecChannels[iCurChanID].PrepAndSendPacket ( &Socket, + vecvecbyCodedData[iChanCnt], + iCeltNumCodedBytes ); + // FALLTHROUGH + case 0: + // send separate mix to current clients + vecChannels[iCurChanID].PrepAndSendPacket ( &Socket, + vecvecbyCodedData[iChanCnt], + iCeltNumCodedBytes ); + // FALLTHROUGH + default: + break; + } } } @@ -1727,3 +1747,19 @@ bool CServer::CreateLevelsForAllConChannels ( const int i return bLevelsWereUpdated; } + +void CServer :: OnCLPingWithAdjustmentReceived ( CHostAddress InetAddr, int iMs, float fAdjust ) +{ + const int iCurChanID = FindChannel ( InetAddr ); + + if ( iCurChanID != INVALID_CHANNEL_ID ) + { + vecChannels[iCurChanID].SetLocalAdjustment ( fAdjust ); + fAdjust = vecChannels[iCurChanID].GetPeerAdjustment (); + } + else + { + fAdjust = 0.0f; + } + ConnLessProtocol.CreateCLPingWithAdjustment ( InetAddr, iMs, fAdjust ); +} diff --git a/src/server.h b/src/server.h index 76776b516d..c8d90f1942 100755 --- a/src/server.h +++ b/src/server.h @@ -444,6 +444,7 @@ public slots: void OnCLPingReceived ( CHostAddress InetAddr, int iMs ) { ConnLessProtocol.CreateCLPingMes ( InetAddr, iMs ); } + void OnCLPingWithAdjustmentReceived ( CHostAddress InetAddr, int iMs, float fAdjust ); void OnCLPingWithNumClientsReceived ( CHostAddress InetAddr, int iMs, int )