mirror of https://github.com/PCSX2/pcsx2.git
SPU2X: New (simpler) time stretcher algorithm.
Should prevent erratic tempo variations near 100% speeds (compared to the previous implementation), and generally allow much lower latencies while keeping the audio stable (on all audio systems). E.g. my tests show that at 70ms latency it can properly handle sharp speed variance of 50%-100%. If your game generally doesn't get slower than 70% speed, 50ms latency would probably be fine. Note that on extreme scenarios (e.g. extreme sharp drops in speed) it doesn't try to keep the audio continues at all costs. If your game exhibits such behavior (audible clicks, etc), increasing the latency a bit at the config would help. Comments on actual performance/quality on different systems would be greatly appreciated. git-svn-id: http://pcsx2.googlecode.com/svn/trunk@4820 96395faa-99c1-11dd-bbfe-3dabce05a288
This commit is contained in:
parent
9a16937018
commit
7cb4dbb8d9
|
@ -382,6 +382,7 @@ private:
|
|||
static void PredictDataWrite( int samples );
|
||||
static float GetStatusPct();
|
||||
static void UpdateTempoChangeSoundTouch();
|
||||
static void UpdateTempoChangeSoundTouch2();
|
||||
|
||||
public:
|
||||
static void UpdateTempoChangeAsyncMixing();
|
||||
|
|
|
@ -17,6 +17,10 @@
|
|||
|
||||
#include "Global.h"
|
||||
#include "soundtouch/SoundTouch.h"
|
||||
#include <wx/datetime.h>
|
||||
|
||||
//Uncomment the next line to use the old time stretcher
|
||||
//#define SPU2X_USE_OLD_STRETCHER
|
||||
|
||||
static soundtouch::SoundTouch* pSoundTouch = NULL;
|
||||
static int ts_stats_stretchblocks = 0;
|
||||
|
@ -59,6 +63,131 @@ float SndBuffer::GetStatusPct()
|
|||
return result;
|
||||
}
|
||||
|
||||
|
||||
//Alternative simple tempo adjustment. Based only on the soundtouch buffer state.
|
||||
//Base algorithm: aim at specific % filled of that buffer, and adjust tempo simply by current/target of % fill.
|
||||
//An extra mechanism is added to keep adjustment at perfect 1:1 ratio (when emulation speed is stable around 100%)
|
||||
// to prevent constant stretching/shrinking of packets if possible.
|
||||
// This mechanism is triggered when the adjustment is close to 1:1 for long enough (defaults to 100 iterations within hys_ok_factor - defaults to 3%).
|
||||
// 1:1 state is aborted when required adjustment goes beyond hys_bad_factor (defaults to 20%).
|
||||
//
|
||||
//To compensate for wide variation of the current buffer ratio due to relatively small size of the buffer,
|
||||
// The required tempo is a running average of STRETCH_AVERAGE_LEN (defaults to 50) last calculations.
|
||||
// This averaging slows down the respons time of the algorithm, but greatly stablize it towards steady stretching.
|
||||
//
|
||||
// Note, this algorithm is intentionally simplified by not taking extreme actions at extreme scenarios (mostly underruns when speed dtops sharply),
|
||||
// and let's the overrun/underrun protections do what they should (doesn't happen much though in practice, even at big FPS variations).
|
||||
//
|
||||
// These params were tested to show good respond and stability, on all audio systems (dsound, wav, port audio, xaudio2),
|
||||
// even at extreme small latency of 50ms which can handle 50%-100% variations without audible glitches.
|
||||
|
||||
|
||||
//running average can be implemented in O(1) time.
|
||||
//For the sake of simplicity, this average is calculated in O(<buffer-size>). Possibly improve later.
|
||||
#define STRETCH_AVERAGE_LEN 50
|
||||
//adds a value to the running average buffer, and return new running average.
|
||||
float addToAvg(float val){
|
||||
static float avg_fullness[STRETCH_AVERAGE_LEN]={0};
|
||||
static int nextAvgPos=0;
|
||||
|
||||
avg_fullness[nextAvgPos]=val;
|
||||
nextAvgPos=(nextAvgPos+1)%STRETCH_AVERAGE_LEN;
|
||||
float sum=0;
|
||||
for(int i=0; i<STRETCH_AVERAGE_LEN; i++)
|
||||
sum+=avg_fullness[i];
|
||||
|
||||
sum= (float)sum/(float)STRETCH_AVERAGE_LEN;
|
||||
return sum?sum:1;
|
||||
}
|
||||
|
||||
template <class T>
|
||||
T clamp(T val, T min, T max){
|
||||
if( val > max ) return max;
|
||||
if( val < min ) return min;
|
||||
return val;
|
||||
}
|
||||
|
||||
//actual stretch algorithm implementation
|
||||
void SndBuffer::UpdateTempoChangeSoundTouch2()
|
||||
{
|
||||
//base aim at buffer filled %
|
||||
float targetFullness=0.1;
|
||||
|
||||
//threshold params (hysteresis)
|
||||
static const float hys_ok_factor=1.03;
|
||||
static const int hys_min_ok_count=100; //consecutive iterations within hys_ok before going to 1:1 mode
|
||||
static const float hys_bad_factor=1.2;
|
||||
|
||||
//state vars
|
||||
static bool inside_hysteresis=false;
|
||||
static int hys_ok_count=0;
|
||||
|
||||
//some precalculated values
|
||||
static const float hys_ok_min=1.0/hys_ok_factor;
|
||||
static const float hys_ok_max=hys_ok_factor;
|
||||
static const float hys_bad_min=1.0/hys_bad_factor;
|
||||
static const float hys_bad_max=hys_bad_factor;
|
||||
|
||||
float bufferFullness=(float)m_data/(float)m_size;
|
||||
static float last_bufferFullness=0;
|
||||
if(last_bufferFullness != bufferFullness){// only recalculate if buffer changes
|
||||
last_bufferFullness = bufferFullness;
|
||||
|
||||
float tempoAdjust=bufferFullness/targetFullness;
|
||||
float avgerage = addToAvg(tempoAdjust);
|
||||
tempoAdjust = avgerage;
|
||||
if( tempoAdjust>1.2 ) tempoAdjust=0.2+pow(tempoAdjust-0.2f, 3);//reduce latency for faster speeds only
|
||||
tempoAdjust = clamp( tempoAdjust, 0.1f, 10.0f);
|
||||
|
||||
if( !inside_hysteresis )
|
||||
{
|
||||
if( tempoAdjust == clamp( tempoAdjust, hys_ok_min, hys_ok_max ) )
|
||||
hys_ok_count++;
|
||||
else
|
||||
hys_ok_count=0;
|
||||
|
||||
if( hys_ok_count >= hys_min_ok_count ){
|
||||
inside_hysteresis=true;
|
||||
if(MsgOverruns()) printf("======> stretch: None (1:1)\n");
|
||||
}
|
||||
|
||||
}
|
||||
else if( tempoAdjust != clamp( tempoAdjust, hys_bad_min, hys_bad_max ) ){
|
||||
if(MsgOverruns()) printf("~~~~~~> stretch: Dynamic\n");
|
||||
inside_hysteresis=false;
|
||||
hys_ok_count=0;
|
||||
}
|
||||
|
||||
if(inside_hysteresis)
|
||||
tempoAdjust=1.0;
|
||||
|
||||
if(MsgOverruns()){
|
||||
static int iters=0;
|
||||
static wxDateTime last=wxDateTime::UNow();
|
||||
wxDateTime unow=wxDateTime::UNow();
|
||||
wxTimeSpan delta = unow.Subtract(last);
|
||||
|
||||
if(delta.GetMilliseconds()>1000){//report buffers state and tempo adjust every second
|
||||
printf("buffers: %f, actual adjust: %f, iterations: %d\n", bufferFullness, tempoAdjust, iters);
|
||||
last=unow;
|
||||
iters=0;
|
||||
}
|
||||
iters++;
|
||||
}
|
||||
|
||||
pSoundTouch->setTempo(tempoAdjust);
|
||||
|
||||
//collect some stats...
|
||||
if(tempoAdjust==1.0)
|
||||
ts_stats_normalblocks++;
|
||||
else
|
||||
ts_stats_stretchblocks++;
|
||||
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
void SndBuffer::UpdateTempoChangeSoundTouch()
|
||||
{
|
||||
float statusPct = GetStatusPct();
|
||||
|
@ -292,7 +421,11 @@ void SndBuffer::timeStretchWrite()
|
|||
progress = true;
|
||||
}
|
||||
|
||||
#ifdef SPU2X_USE_OLD_STRETCHER
|
||||
UpdateTempoChangeSoundTouch();
|
||||
#else
|
||||
UpdateTempoChangeSoundTouch2();
|
||||
#endif
|
||||
|
||||
if( MsgOverruns() )
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue