void SndBuffer::_WriteSamples(StereoOut32 *bData, int nSamples) { m_predictData = 0; // Problem: // If the SPU2 gets out of sync with the SndOut device, the writepos of the // circular buffer will overtake the readpos, leading to a prolonged period // of hopscotching read/write accesses (ie, lots of staticy crap sound for // several seconds). // // Compromise: // When an overrun occurs, we adapt by discarding a portion of the buffer. // The older portion of the buffer is discarded rather than incoming data, // so that the overall audio synchronization is better. int free = m_size - _GetApproximateDataInBuffer(); // -1, but the <= handles that if( free <= nSamples ) { // Disabled since the lock-free queue can't handle changing the read end from the write thread #if 0 // Buffer overrun! // Dump samples from the read portion of the buffer instead of dropping // the newly written stuff. s32 comp; if( SynchMode == 0 ) // TimeStrech on { comp = timeStretchOverrun(); } else { // Toss half the buffer plus whatever's being written anew: comp = GetAlignedBufferSize( (m_size + nSamples ) / 16 ); if( comp > (m_size-SndOutPacketSize) ) comp = m_size-SndOutPacketSize; } _DropSamples_Internal(comp); if( MsgOverruns() ) ConLog(" * SPU2 > Overrun Compensation (%d packets tossed)\n", comp / SndOutPacketSize ); lastPct = 0.0; // normalize the timestretcher #else if( MsgOverruns() ) ConLog(" * SPU2 > Overrun! 1 packet tossed)\n"); lastPct = 0.0; // normalize the timestretcher return; #endif } _WriteSamples_Safe(bData, nSamples); }
void SndBuffer::timeStretchWrite() { bool progress = false; // data prediction helps keep the tempo adjustments more accurate. // The timestretcher returns packets in belated "clump" form. // Meaning that most of the time we'll get nothing back, and then // suddenly we'll get several chunks back at once. Thus we use // data prediction to make the timestretcher more responsive. PredictDataWrite( (int)( SndOutPacketSize / eTempo ) ); CvtPacketToFloat( sndTempBuffer ); pSoundTouch->putSamples( (float*)sndTempBuffer, SndOutPacketSize ); int tempProgress; while( tempProgress = pSoundTouch->receiveSamples( (float*)sndTempBuffer, SndOutPacketSize), tempProgress != 0 ) { // Hint: It's assumed that pSoundTouch will return chunks of 128 bytes (it always does as // long as the SSE optimizations are enabled), which means we can do our own SSE opts here. CvtPacketToInt( sndTempBuffer, tempProgress ); _WriteSamples( sndTempBuffer, tempProgress ); progress = true; } #ifdef SPU2X_USE_OLD_STRETCHER UpdateTempoChangeSoundTouch(); #else UpdateTempoChangeSoundTouch2(); #endif if( MsgOverruns() ) { if( progress ) { if( ++ts_stats_logcounter > 150 ) { ts_stats_logcounter = 0; ConLog( " * SPU2 > Timestretch Stats > %d percent stretched. Total stretchblocks = %d.\n", ( ts_stats_stretchblocks * 100 ) / ( ts_stats_normalblocks + ts_stats_stretchblocks ), ts_stats_stretchblocks); ts_stats_normalblocks = 0; ts_stats_stretchblocks = 0; } } } }
// Returns TRUE if there is data to be output, or false if no data // is available to be copied. bool SndBuffer::CheckUnderrunStatus( int& nSamples, int& quietSampleCount ) { quietSampleCount = 0; int data = _GetApproximateDataInBuffer(); if( m_underrun_freeze ) { int toFill = m_size / ( (SynchMode == 2) ? 32 : 400); // TimeStretch and Async off? toFill = GetAlignedBufferSize( toFill ); // toFill is now aligned to a SndOutPacket if( data < toFill ) { quietSampleCount = nSamples; return false; } m_underrun_freeze = false; if( MsgOverruns() ) ConLog(" * SPU2 > Underrun compensation (%d packets buffered)\n", toFill / SndOutPacketSize ); lastPct = 0.0; // normalize timestretcher } else if( data < nSamples ) { nSamples = data; quietSampleCount = SndOutPacketSize - data; m_underrun_freeze = true; if( SynchMode == 0 ) // TimeStrech on timeStretchUnderrun(); return nSamples != 0; } return true; }
//actual stretch algorithm implementation void SndBuffer::UpdateTempoChangeSoundTouch2() { long targetSamplesReservoir = 48 * SndOutLatencyMS; //48000*SndOutLatencyMS/1000 //base aim at buffer filled % float baseTargetFullness = (double)targetSamplesReservoir; ///(double)m_size;//0.05; //state vars static bool inside_hysteresis; //=false; static int hys_ok_count; //=0; static float dynamicTargetFullness; //=baseTargetFullness; if (gRequestStretcherReset >= STRETCHER_RESET_THRESHOLD) { ConLog("______> stretch: Reset.\n"); inside_hysteresis = false; hys_ok_count = 0; dynamicTargetFullness = baseTargetFullness; } int data = _GetApproximateDataInBuffer(); float bufferFullness = (float)data; ///(float)m_size; #ifdef NEWSTRETCHER_USE_DYNAMIC_TUNING { //test current iterations/sec every 0.5s, and change algo params accordingly if different than previous IPS more than 30% static long iters = 0; static wxDateTime last = wxDateTime::UNow(); wxDateTime unow = wxDateTime::UNow(); wxTimeSpan delta = unow.Subtract(last); if (delta.GetMilliseconds() > 500) { int pot_targetIPS = 1000.0 / delta.GetMilliseconds().ToDouble() * iters; if (!IsInRange(pot_targetIPS, int((float)targetIPS / 1.3f), int((float)targetIPS * 1.3f))) { if (MsgOverruns()) ConLog("Stretcher: setting iters/sec from %d to %d\n", targetIPS, pot_targetIPS); targetIPS = pot_targetIPS; AVERAGING_WINDOW = GetClamped((int)(50.0f * (float)targetIPS / 750.0f), 3, (int)AVERAGING_BUFFER_SIZE); } last = unow; iters = 0; } iters++; } #endif //Algorithm params: (threshold params (hysteresis), etc) const float hys_ok_factor = 1.04f; const float hys_bad_factor = 1.2f; int hys_min_ok_count = GetClamped((int)(50.0 * (float)targetIPS / 750.0), 2, 100); //consecutive iterations within hys_ok before going to 1:1 mode int compensationDivider = GetClamped((int)(100.0 * (float)targetIPS / 750), 15, 150); float tempoAdjust = bufferFullness / dynamicTargetFullness; float avgerage = addToAvg(tempoAdjust); tempoAdjust = avgerage; // Dampen the adjustment to avoid overshoots (this means the average will compensate to the other side). // This is different than simply bigger averaging window since bigger window also has bigger "momentum", // so it's slower to slow down when it gets close to the equilibrium state and can therefore resonate. // The dampening (sqrt was chosen for no very good reason) manages to mostly prevent that. tempoAdjust = sqrt(tempoAdjust); tempoAdjust = GetClamped(tempoAdjust, 0.05f, 10.0f); if (tempoAdjust < 1) baseTargetFullness /= sqrt(tempoAdjust); // slightly increase latency when running slow. dynamicTargetFullness += (baseTargetFullness / tempoAdjust - dynamicTargetFullness) / (double)compensationDivider; if (IsInRange(tempoAdjust, 0.9f, 1.1f) && IsInRange(dynamicTargetFullness, baseTargetFullness * 0.9f, baseTargetFullness * 1.1f)) dynamicTargetFullness = baseTargetFullness; if (!inside_hysteresis) { if (IsInRange(tempoAdjust, 1.0f / hys_ok_factor, hys_ok_factor)) hys_ok_count++; else hys_ok_count = 0; if (hys_ok_count >= hys_min_ok_count) { inside_hysteresis = true; if (MsgOverruns()) ConLog("======> stretch: None (1:1)\n"); } } else if (!IsInRange(tempoAdjust, 1.0f / hys_bad_factor, hys_bad_factor)) { if (MsgOverruns()) ConLog("~~~~~~> 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 ConLog("buffers: %4d ms (%3.0f%%), tempo: %f, comp: %2.3f, iters: %d, (N-IPS:%d -> avg:%d, minokc:%d, div:%d) reset:%d\n", (int)(data / 48), (double)(100.0 * bufferFullness / baseTargetFullness), (double)tempoAdjust, (double)(dynamicTargetFullness / baseTargetFullness), iters, (int)targetIPS, AVERAGING_WINDOW, hys_min_ok_count, compensationDivider, gRequestStretcherReset); last = unow; iters = 0; } iters++; } pSoundTouch->setTempo(tempoAdjust); if (gRequestStretcherReset >= STRETCHER_RESET_THRESHOLD) gRequestStretcherReset = 0; return; }
//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; }