diff --git a/stepmania/Themes/default/Languages/de.ini b/stepmania/Themes/default/Languages/de.ini index be0b519832..c95da71654 100644 --- a/stepmania/Themes/default/Languages/de.ini +++ b/stepmania/Themes/default/Languages/de.ini @@ -1,12 +1,12 @@ [AdjustSync] -%s tempo segment from %+.3f BPM to %+.3f BPM.=%s Temposegment von %+.3f BPM auf %+.3f BPM. +%s BPM from %.3f BPM to %.3f BPM.=%s BPM von %.3f BPM auf %.3f BPM. Autosync Machine=Programm automatisch synchronisieren Autosync Song=Lied automatisch synchronisieren Autosync: Correction NOT applied. Deviation too high.=Keine Korrektur der Synchronisation möglich, Abweichung zu groß. Autosync: Correction applied.=Automatische Synchronisation erfolgreich ausgeführt. Global Offset from %+.3f to %+.3f (notes %s)=Globaler Offset von %+.3f auf %+.3f (Noten %s) Song offset from %+.3f to %+.3f (notes %s)=Songoffset von %+.3f auf %+.3f (Noten %s) -The stop segment #%d changed from %+.3f seconds to %+.3f seconds (change of %+.3f).=Das Stopsegment Nummer #%d wurde geändert von %+.3f Sekunden auf %+.3f Sekunden (Veränderung von %+.3f). +The stop segment #%d changed from %+.3fs to %+.3fs (change of %+.3f).=Das Stopsegment Nummer #%d wurde geändert von %+.3f Sekunden auf %+.3f Sekunden (Veränderung von %+.3f). earlier=früher later=später diff --git a/stepmania/Themes/default/Languages/en.ini b/stepmania/Themes/default/Languages/en.ini index 20887f5874..87e9c1dec2 100644 --- a/stepmania/Themes/default/Languages/en.ini +++ b/stepmania/Themes/default/Languages/en.ini @@ -605,6 +605,7 @@ SuperShuffle=S.Shuffle Swap Sides=Swap Sides Sync Machine=Sync Machine Sync Song=Sync Song +Sync Tempo=Sync Tempo Tipsy=Tipsy Title Only=Title Only Tornado=Tornado @@ -893,16 +894,20 @@ Rave=Rave Regular=Regular [AdjustSync] +%s BPM from %.3f BPM to %.3f BPM.=%s BPM from %.3f BPM to %.3f BPM. Autosync Machine=Autosync Machine Autosync Song=Autosync Song +Autosync Tempo=Autosync Tempo Autosync: Correction NOT applied. Deviation too high.=Autosync: Correction NOT applied. Deviation too high. Autosync: Correction applied.=Autosync: Correction applied. -The stop segment #%d changed from %+.3f seconds to %+.3f seconds (change of %+.3f).=The stop segment number #%d changed from %+.3f seconds to %+.3f seconds (change of %+.3f). -earlier=earlier -later=later -%s tempo segment from %+.3f BPM to %+.3f BPM.=%s tempo segment from %+.3f BPM to %+.3f BPM. Global Offset from %+.3f to %+.3f (notes %s)=Global Offset from %+.3f to %+.3f (notes %s) Song offset from %+.3f to %+.3f (notes %s)=Song offset from %+.3f to %+.3f (notes %s) +The stop segment #%d changed from %+.3fs to %+.3fs (change of %+.3f).=The stop segment number #%d changed from %+.3fs to %+.3fs (change of %+.3f). +earlier=earlier +later=later +Average Error %.5fs=Average Error %.5fs +Etc.=Etc. +%d taps ignored.=%d taps ignored. [ProfileAction] Clear=Clear @@ -999,6 +1004,7 @@ Screen Test Mode=Screen Test Mode Send Notes Ended=Send Notes Ended Slow=Slow Song=Song +Tempo=Tempo Uptime=Uptime Volume Down=Volume Down Volume Up=Volume Up @@ -1354,6 +1360,7 @@ AutoPlay=AutoPlay AutoPlayCPU=AutoPlayCPU AutoSync Machine=AutoSync Machine AutoSync Song=AutoSync Song +AutoSync Tempo=AutoSync Tempo Can't sync while playing a course.=Can't sync while playing a course. Sync changes reverted.=Sync changes reverted. Revert sync changes=Revert sync changes diff --git a/stepmania/Themes/default/Languages/es.ini b/stepmania/Themes/default/Languages/es.ini index 01a1059397..e44136df96 100644 --- a/stepmania/Themes/default/Languages/es.ini +++ b/stepmania/Themes/default/Languages/es.ini @@ -1,12 +1,12 @@ [AdjustSync] -%s tempo segment from %+.3f BPM to %+.3f BPM.=Segmento de tempo %s desde %+.3f BPM a %+.3f BPM. +%s BPM from %.3f BPM to %.3f BPM.=BPM %s desde %.3f BPM a %.3f BPM. Autosync Machine=Máquina Autosync Autosync Song=Canción Autosync Autosync: Correction NOT applied. Deviation too high.=Autosync: Corrección NO aplicada. Desviación demasiado alta. Autosync: Correction applied.=Autosync: Corrección aplicada. Global Offset from %+.3f to %+.3f (notes %s)=Offset Global de %+.3f a %+.3f (notas %s) Song offset from %+.3f to %+.3f (notes %s)=Offset de Canción de %+.3f a %+.3f (notas %s) -The stop segment #%d changed from %+.3f seconds to %+.3f seconds (change of %+.3f).=El número de segmento detenido #%d cambio de %+.3f segundos a %+.3f segundos (cambio de %+.3f). +The stop segment #%d changed from %+.3fs to %+.3fs (change of %+.3f).=El número de segmento detenido #%d cambio de %+.3fs a %+.3fs (cambio de %+.3f). earlier=antes later=después diff --git a/stepmania/Themes/default/Languages/fr.ini b/stepmania/Themes/default/Languages/fr.ini index dde76730e0..6f12771782 100644 --- a/stepmania/Themes/default/Languages/fr.ini +++ b/stepmania/Themes/default/Languages/fr.ini @@ -1,12 +1,12 @@ [AdjustSync] -%s tempo segment from %+.3f BPM to %+.3f BPM.=%s segment de tempo de %+.3f BPM à %+.3f BPM. +%s BPM from %.3f BPM to %.3f BPM.=%s BPM de %.3f BPM à %.3f BPM. Autosync Machine=Console d'autosync Autosync Song=Chanson d'autosync Autosync: Correction NOT applied. Deviation too high.=Autosync.: Correction NON appliquée. Déviation trop importante. Autosync: Correction applied.=Autosync.: Correction appliquée. Global Offset from %+.3f to %+.3f (notes %s)=Délai global de %+.3f à %+.3f (notes %s) Song offset from %+.3f to %+.3f (notes %s)=Délai chanson de %+.3f à %+.3f (notes %s) -The stop segment #%d changed from %+.3f seconds to %+.3f seconds (change of %+.3f).=Le segment stop numéro #%d a changé de %+.3f secondes à %+.3f secondes (changement de %+.3f). +The stop segment #%d changed from %+.3fs to %+.3fs (change of %+.3f).=Le segment stop numéro #%d a changé de %+.3fs à %+.3fs (changement de %+.3f). earlier=plus tôt later=plus tard diff --git a/stepmania/Themes/default/Languages/it.ini b/stepmania/Themes/default/Languages/it.ini index fe5518df27..b60270277c 100644 --- a/stepmania/Themes/default/Languages/it.ini +++ b/stepmania/Themes/default/Languages/it.ini @@ -1,12 +1,12 @@ [AdjustSync] -%s tempo segment from %+.3f BPM to %+.3f BPM.=Segmento tempo %s da %+.3f BPM a %+.3f BPM. +%s BPM from %.3f BPM to %.3f BPM.=BPM %s da %.3f BPM a %.3f BPM. Autosync Machine=Macchina Autosincronizzazione Autosync Song=Canzone Autosincronizzazione Autosync: Correction NOT applied. Deviation too high.=Autosincronizzazione: correzione NON effettuata. Troppo differente. Autosync: Correction applied.=Autosync: correzione effettuata. Global Offset from %+.3f to %+.3f (notes %s)=Offset Globale da %+.3f to %+.3f ( note %s). Song offset from %+.3f to %+.3f (notes %s)=Offset Canzone da %+.3f to %+.3f ( note %s). -The stop segment #%d changed from %+.3f seconds to %+.3f seconds (change of %+.3f).=Il numero del segmento di stop #%d è cambiato da %+.3f BPS a %+.3f BPS (cambio di %+.3f). +The stop segment #%d changed from %+.3fs to %+.3fs (change of %+.3f).=Il numero del segmento di stop #%d è cambiato da %+.3fs a %+.3fs (cambio di %+.3f). earlier=prima later=dopo diff --git a/stepmania/Themes/default/metrics.ini b/stepmania/Themes/default/metrics.ini index 55d5ebc74e..c1a11d46dc 100644 --- a/stepmania/Themes/default/metrics.ini +++ b/stepmania/Themes/default/metrics.ini @@ -3982,11 +3982,12 @@ Rate,15="mod,1.7xmusic;name,1.7x" Rate,16="mod,1.8xmusic;name,1.8x" Rate,17="mod,1.9xmusic;name,1.9x" Rate,18="mod,2.0xmusic;name,2.0x" -AutoAdjust="3;together" +AutoAdjust="4;together" AutoAdjustDefault="" AutoAdjust,1="mod,no autosync;name,Off" AutoAdjust,2="mod,autosyncsong;name,Sync Song" AutoAdjust,3="mod,autosyncmachine;name,Sync Machine" +AutoAdjust,4="mod,autosynctempo;name,Sync Tempo" SaveScores="2;together" SaveScoresDefault="" SaveScores,1="mod,no savescore;name,Off" diff --git a/stepmania/src/AdjustSync.cpp b/stepmania/src/AdjustSync.cpp index 115b7ac745..9cad4e93b7 100644 --- a/stepmania/src/AdjustSync.cpp +++ b/stepmania/src/AdjustSync.cpp @@ -1,3 +1,38 @@ +/* + * AdjustSync defines two methods for fixing the sync. + * + * The first method adjusts either the song or the machine by the + * average offset of the user's steps. In other words, if the user + * averages to step early by 10 ms, either the song or the global + * offset is adjusted by 10 ms to compensate for that. These + * adjustments only require a small set of data, so this method + * updates the offset while the song is playing. + * + * The second method adjusts both the offset and the tempo of an + * individual song. It records all of the steps during a play of + * the song and uses linear least squares regression to minimize the + * error of those steps. It makes one adjustment for the tempo of + * the entire song, rather than adding many different tempo segments + * to match the steps. If there are already several tempo segments + * in the stepfile, this method makes a proportional change to each + * of them. For example, if it changes 100 bpm to 101 bpm, it will + * also change 200 bpm to 202 bpm. This method also adjusts the stops. + * It assumes that a given stop is measured in terms of beats and makes + * the appropriate change. + * + * If we use this method on a small set of data late in the song, it + * can have very chaotic effects on the early settings. For example, + * it may change the offset by several hundred milliseconds and make a + * large change to the BPM to compensate if that would minimize the + * error. This problem occurs especially when the user makes a couple + * steps that are significantly off beat. The way to avoid this is to + * perform the least squares regression once on all of the data + * collected, rather than adjusting the sync every time we get another + * 50 or so data points. In fact, if we are playing in edit mode and + * the user loops through the song more than once, we use all of the + * steps made. + */ + #include "global.h" #include "AdjustSync.h" #include "GameState.h" @@ -7,10 +42,15 @@ #include "ScreenManager.h" TimingData *AdjustSync::s_pTimingDataOriginal = NULL; -float AdjustSync::s_fGlobalOffsetSecondsOriginal = 0; +float AdjustSync::s_fGlobalOffsetSecondsOriginal = 0.0f; int AdjustSync::s_iAutosyncOffsetSample = 0; -float AdjustSync::s_fAutosyncOffset[SAMPLE_COUNT]; -float AdjustSync::s_fStandardDeviation = 0; +float AdjustSync::s_fAutosyncOffset[AdjustSync::OFFSET_SAMPLE_COUNT]; +float AdjustSync::s_fStandardDeviation = 0.0f; +vector< pair > AdjustSync::s_vAutosyncTempoData; +float AdjustSync::s_fAverageError = 0.0f; +const float AdjustSync::ERROR_TOO_HIGH = 0.025f; +int AdjustSync::s_iStepsFiltered = 0; + void AdjustSync::ResetOriginalSyncData() { @@ -23,7 +63,13 @@ void AdjustSync::ResetOriginalSyncData() *s_pTimingDataOriginal = TimingData(); s_fGlobalOffsetSecondsOriginal = PREFSMAN->m_fGlobalOffsetSeconds; + ResetAutosync(); +} + +void AdjustSync::ResetAutosync() +{ s_iAutosyncOffsetSample = 0; + s_vAutosyncTempoData.clear(); } bool AdjustSync::IsSyncDataChanged() @@ -46,7 +92,8 @@ void AdjustSync::SaveSyncChanges() if( s_fGlobalOffsetSecondsOriginal != PREFSMAN->m_fGlobalOffsetSeconds ) PREFSMAN->SavePrefsToDisk(); ResetOriginalSyncData(); - s_fStandardDeviation = 0; + s_fStandardDeviation = 0.0f; + s_fAverageError = 0.0f; } void AdjustSync::RevertSyncChanges() @@ -55,26 +102,59 @@ void AdjustSync::RevertSyncChanges() return; PREFSMAN->m_fGlobalOffsetSeconds.Set( s_fGlobalOffsetSecondsOriginal ); GAMESTATE->m_pCurSong->m_Timing = *s_pTimingDataOriginal; - s_fStandardDeviation = 0; + ResetOriginalSyncData(); + s_fStandardDeviation = 0.0f; + s_fAverageError = 0.0f; } static LocalizedString AUTOSYNC_CORRECTION_APPLIED ( "AdjustSync", "Autosync: Correction applied." ); static LocalizedString AUTOSYNC_CORRECTION_NOT_APPLIED ( "AdjustSync", "Autosync: Correction NOT applied. Deviation too high." ); static LocalizedString AUTOSYNC_SONG ( "AdjustSync", "Autosync Song" ); static LocalizedString AUTOSYNC_MACHINE ( "AdjustSync", "Autosync Machine" ); -void AdjustSync::HandleAutosync( float fNoteOffBySeconds ) +static LocalizedString AUTOSYNC_TEMPO ( "AdjustSync", "Autosync Tempo" ); +void AdjustSync::HandleAutosync( float fNoteOffBySeconds, float fStepTime ) { - if( GAMESTATE->m_SongOptions.GetCurrent().m_AutosyncType == SongOptions::AUTOSYNC_OFF ) + switch( GAMESTATE->m_SongOptions.GetCurrent().m_AutosyncType ) { + case SongOptions::AUTOSYNC_OFF: return; + case SongOptions::AUTOSYNC_TEMPO: + { + // We collect all of the data and process it at the end + s_vAutosyncTempoData.push_back( make_pair(fStepTime, fNoteOffBySeconds) ); + break; + } + case SongOptions::AUTOSYNC_MACHINE: + case SongOptions::AUTOSYNC_SONG: + { + s_fAutosyncOffset[s_iAutosyncOffsetSample] = fNoteOffBySeconds; + ++s_iAutosyncOffsetSample; - s_fAutosyncOffset[s_iAutosyncOffsetSample] = fNoteOffBySeconds; - s_iAutosyncOffsetSample++; + if( s_iAutosyncOffsetSample < OFFSET_SAMPLE_COUNT ) + break; // need more - if( s_iAutosyncOffsetSample < SAMPLE_COUNT ) - return; /* need more */ + AutosyncOffset(); + break; + } + default: + ASSERT(0); + } +} - const float mean = calc_mean( s_fAutosyncOffset, s_fAutosyncOffset+SAMPLE_COUNT ); - const float stddev = calc_stddev( s_fAutosyncOffset, s_fAutosyncOffset+SAMPLE_COUNT ); +void AdjustSync::HandleSongEnd() +{ + if( GAMESTATE->m_SongOptions.GetCurrent().m_AutosyncType == SongOptions::AUTOSYNC_TEMPO ) + { + AutosyncTempo(); + } + + // all other states don't care +} + +void AdjustSync::AutosyncOffset() +{ + + const float mean = calc_mean( s_fAutosyncOffset, s_fAutosyncOffset+OFFSET_SAMPLE_COUNT ); + const float stddev = calc_stddev( s_fAutosyncOffset, s_fAutosyncOffset+OFFSET_SAMPLE_COUNT ); RString sAutosyncType; switch( GAMESTATE->m_SongOptions.GetCurrent().m_AutosyncType ) @@ -89,7 +169,7 @@ void AdjustSync::HandleAutosync( float fNoteOffBySeconds ) ASSERT(0); } - if( stddev < .03 ) // If they stepped with less than .03 error + if( stddev < .03f ) // If they stepped with less than .03 error { switch( GAMESTATE->m_SongOptions.GetCurrent().m_AutosyncType ) { @@ -114,13 +194,79 @@ void AdjustSync::HandleAutosync( float fNoteOffBySeconds ) s_fStandardDeviation = stddev; } +void AdjustSync::AutosyncTempo() +{ + float fSlope = 0.0f; + float fIntercept = 0.0f; + if( !CalcLeastSquares( s_vAutosyncTempoData, + &fSlope, &fIntercept, &s_fAverageError ) ) + return; + + if( s_fAverageError < ERROR_TOO_HIGH ) + { + // Here we filter out any steps that are too far off. + // + // If it turns out that we want to be even more selective, we can + // keep only a fraction of the data, such as the 80% with the lowest + // error. However, throwing away the ones with high error should + // be enough in most cases. + float fFilteredError = 0.0; + s_iStepsFiltered = s_vAutosyncTempoData.size(); + FilterHighErrorPoints( &s_vAutosyncTempoData, + fSlope, fIntercept, ERROR_TOO_HIGH ); + s_iStepsFiltered -= s_vAutosyncTempoData.size(); + + if( !CalcLeastSquares( s_vAutosyncTempoData, + &fSlope, &fIntercept, &fFilteredError ) ) + return; + + GAMESTATE->m_pCurSong->m_Timing.m_fBeat0OffsetInSeconds += fIntercept; + + vector::iterator itBPM; + for( itBPM = GAMESTATE->m_pCurSong->m_Timing.m_BPMSegments.begin(); + itBPM != GAMESTATE->m_pCurSong->m_Timing.m_BPMSegments.end(); + ++itBPM ) + { + itBPM->SetBPM( 60.0f / ((60.0f / itBPM->GetBPM()) * (1.0f - fSlope)) ); + } + + // We assume that the stops were measured as a number of beats. + // Therefore, if we change the bpms, we need to make a similar + // change to the stops. + vector::iterator itStop; + for( itStop = GAMESTATE->m_pCurSong->m_Timing.m_StopSegments.begin(); + itStop != GAMESTATE->m_pCurSong->m_Timing.m_StopSegments.end(); + ++itStop ) + { + itStop->m_fStopSeconds = itStop->m_fStopSeconds * ( 1.0f - fSlope ); + } + + SCREENMAN->SystemMessage( AUTOSYNC_CORRECTION_APPLIED.GetValue() ); + } + else + { + // deviation... error... close enough for an error message + SCREENMAN->SystemMessage( AUTOSYNC_CORRECTION_NOT_APPLIED.GetValue() ); + } + + s_vAutosyncTempoData.clear(); +} + static LocalizedString EARLIER ("AdjustSync","earlier"); static LocalizedString LATER ("AdjustSync","later"); static LocalizedString GLOBAL_OFFSET_FROM ( "AdjustSync", "Global Offset from %+.3f to %+.3f (notes %s)" ); +// We need to limit the length of lines so each one fits on one line of the SM console. +// The tempo and stop change message can get very long in a complicated song, and at +// a low resolution, the keep/revert menu would be pushed off the bottom of the screen +// if we didn't limit the length of the message. Keeping the lines short lets us fit +// more information on the screen. static LocalizedString SONG_OFFSET_FROM ( "AdjustSync", "Song offset from %+.3f to %+.3f (notes %s)" ); -static LocalizedString TEMPO_SEGMENT_FROM ( "AdjustSync", "%s tempo segment from %+.3f BPM to %+.3f BPM." ); -static LocalizedString CHANGED_STOP ("AdjustSync","The stop segment #%d changed from %+.3f seconds to %+.3f seconds (change of %+.3f)."); +static LocalizedString TEMPO_SEGMENT_FROM ( "AdjustSync", "%s BPM from %.3f BPM to %.3f BPM." ); +static LocalizedString CHANGED_STOP ("AdjustSync","The stop segment #%d changed from %+.3fs to %+.3fs (change of %+.3f)."); +static LocalizedString ERROR ("AdjustSync", "Average Error %.5fs"); +static LocalizedString ETC ("AdjustSync", "Etc."); +static LocalizedString TAPS_IGNORED ("AdjustSync", "%d taps ignored."); void AdjustSync::GetSyncChangeTextGlobal( vector &vsAddTo ) { @@ -129,7 +275,7 @@ void AdjustSync::GetSyncChangeTextGlobal( vector &vsAddTo ) float fNew = Quantize( PREFSMAN->m_fGlobalOffsetSeconds, 0.001f ) ; float fDelta = fNew - fOld; - if( fabs(fDelta) > 0.00001 ) + if( fabsf(fDelta) > 0.00001f ) { vsAddTo.push_back( ssprintf( GLOBAL_OFFSET_FROM.GetValue(), @@ -144,12 +290,14 @@ void AdjustSync::GetSyncChangeTextSong( vector &vsAddTo ) { if( GAMESTATE->m_pCurSong.Get() ) { + unsigned int iOriginalSize = vsAddTo.size(); + { float fOld = Quantize( AdjustSync::s_pTimingDataOriginal->m_fBeat0OffsetInSeconds, 0.001f ); float fNew = Quantize( GAMESTATE->m_pCurSong->m_Timing.m_fBeat0OffsetInSeconds, 0.001f ); float fDelta = fNew - fOld; - if( fabs(fDelta) > 0.00001 ) + if( fabsf(fDelta) > 0.00001f ) { vsAddTo.push_back( ssprintf( SONG_OFFSET_FROM.GetValue(), @@ -161,12 +309,17 @@ void AdjustSync::GetSyncChangeTextSong( vector &vsAddTo ) for( unsigned i=0; im_pCurSong->m_Timing.m_BPMSegments.size(); i++ ) { - float fOld = Quantize( AdjustSync::s_pTimingDataOriginal->m_BPMSegments[i].m_fBPS, 0.001f ); - float fNew = Quantize( GAMESTATE->m_pCurSong->m_Timing.m_BPMSegments[i].m_fBPS, 0.001f ); + float fOld = Quantize( AdjustSync::s_pTimingDataOriginal->m_BPMSegments[i].GetBPM(), 0.001f ); + float fNew = Quantize( GAMESTATE->m_pCurSong->m_Timing.m_BPMSegments[i].GetBPM(), 0.001f ); float fDelta = fNew - fOld; - if( fabs(fDelta) > 0.00001 ) + if( fabsf(fDelta) > 0.00001f ) { + if ( i >= 4 ) + { + vsAddTo.push_back(ETC.GetValue()); + break; + } vsAddTo.push_back( ssprintf( TEMPO_SEGMENT_FROM.GetValue(), FormatNumberAndSuffix(i+1).c_str(), @@ -181,20 +334,34 @@ void AdjustSync::GetSyncChangeTextSong( vector &vsAddTo ) float fNew = Quantize( GAMESTATE->m_pCurSong->m_Timing.m_StopSegments[i].m_fStopSeconds, 0.001f ); float fDelta = fNew - fOld; - if( fabs(fDelta) > 0.00001 ) + if( fabsf(fDelta) > 0.00001f ) { - vsAddTo.push_back( ssprintf( + if ( i >= 4 ) + { + vsAddTo.push_back(ETC.GetValue()); + break; + } + vsAddTo.push_back( ssprintf( CHANGED_STOP.GetValue(), i+1, fOld, fNew ) ); } } + + if( vsAddTo.size() > iOriginalSize && s_fAverageError > 0.0f ) + { + vsAddTo.push_back( ssprintf(ERROR.GetValue(), s_fAverageError) ); + } + if( vsAddTo.size() > iOriginalSize && s_iStepsFiltered > 0 ) + { + vsAddTo.push_back( ssprintf(TAPS_IGNORED.GetValue(), s_iStepsFiltered) ); + } } } /* - * (c) 2003-2004 Chris Danford + * (c) 2003-2006 Chris Danford, John Bauer * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a diff --git a/stepmania/src/AdjustSync.h b/stepmania/src/AdjustSync.h index 953cd59a9b..e242bcaa0d 100644 --- a/stepmania/src/AdjustSync.h +++ b/stepmania/src/AdjustSync.h @@ -1,32 +1,58 @@ #ifndef AdjustSync_H #define AdjustSync_H -class TimingData; +/* + * This class defines two ways of adjusting the sync of a song. + * The first adjusts only the offset, "on the fly". It can adjust + * either the song itself or the machine. The other style adjusts + * both the BPM and the offset of the song, but it needs more data. + */ -const int SAMPLE_COUNT = 56; +class TimingData; class AdjustSync { public: static TimingData *s_pTimingDataOriginal; static float s_fGlobalOffsetSecondsOriginal; + /* + * We only want to call the Reset methods before a song, not immediately after a song. + * If we reset it at the end of a song, we have to carefully check the logic to make + * sure we never reset it before the user gets a chance to save or revert the change. + * Resetting at the start of the song is sufficient. + */ static void ResetOriginalSyncData(); + static void ResetAutosync(); static bool IsSyncDataChanged(); + static void SaveSyncChanges(); static void RevertSyncChanges(); - static void HandleAutosync( float fNoteOffBySeconds ); + static void HandleAutosync( float fNoteOffBySeconds, float fStepTime ); + static void HandleSongEnd(); + static void AutosyncOffset(); + static void AutosyncTempo(); static void GetSyncChangeTextGlobal( vector &vsAddTo ); static void GetSyncChangeTextSong( vector &vsAddTo ); - static float s_fAutosyncOffset[SAMPLE_COUNT]; + static const int OFFSET_SAMPLE_COUNT = 56; + + static float s_fAutosyncOffset[OFFSET_SAMPLE_COUNT]; static int s_iAutosyncOffsetSample; static float s_fStandardDeviation; + + // Measured in seconds. If the average error is too high, we + // reject the recorded data for the Least Squares Regression. + static const float ERROR_TOO_HIGH; + + static vector< pair > s_vAutosyncTempoData; + static float s_fAverageError; + static int s_iStepsFiltered; }; #endif /* - * (c) 2003-2004 Chris Danford + * (c) 2003-2006 Chris Danford, John Bauer * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining a diff --git a/stepmania/src/GameState.cpp b/stepmania/src/GameState.cpp index 3ad51e92be..feac672e22 100644 --- a/stepmania/src/GameState.cpp +++ b/stepmania/src/GameState.cpp @@ -236,6 +236,7 @@ void GameState::Reset() ResetMusicStatistics(); ResetStageStatistics(); + AdjustSync::ResetOriginalSyncData(); FOREACH_PlayerNumber( pn ) PROFILEMAN->UnloadProfile( pn ); @@ -496,6 +497,7 @@ void GameState::BeginStage() LOG->Warn( "XXX: m_iNumStagesOfThisSong == %i?", m_iNumStagesOfThisSong ); ResetStageStatistics(); + AdjustSync::ResetOriginalSyncData(); ARE_STAGE_MODS_FORCED.Read(); // XXX: where does this belong? if( !ARE_STAGE_MODS_FORCED ) @@ -670,8 +672,6 @@ void GameState::ResetStageStatistics() // Reset the round seed. Do this here and not in FinishStage so that players // get new shuffle patterns if they Back out of gameplay and play again. m_iStageSeed = rand(); - - AdjustSync::ResetOriginalSyncData(); } void GameState::UpdateSongPosition( float fPositionSeconds, const TimingData &timing, const RageTimer ×tamp ) diff --git a/stepmania/src/Player.cpp b/stepmania/src/Player.cpp index bf316c5cba..2d5d66a6ed 100644 --- a/stepmania/src/Player.cpp +++ b/stepmania/src/Player.cpp @@ -1056,12 +1056,12 @@ void Player::Step( int col, int row, const RageTimer &tm, bool bHeld, bool bRele { // compute the score for this hit float fNoteOffset = 0.0f; - + // we need this later if we are autosyncing + const float fStepBeat = NoteRowToBeat( iIndexOverlappingNote ); + const float fStepSeconds = GAMESTATE->m_pCurSong->GetElapsedTimeFromBeat(fStepBeat); + if( row == -1 ) { - const float fStepBeat = NoteRowToBeat( iIndexOverlappingNote ); - const float fStepSeconds = GAMESTATE->m_pCurSong->GetElapsedTimeFromBeat(fStepBeat); - /* We actually stepped on the note this long ago: */ const float fTimeSinceStep = tm.Ago(); @@ -1208,7 +1208,7 @@ void Player::Step( int col, int row, const RageTimer &tm, bool bHeld, bool bRele } if( m_pPlayerState->m_PlayerController == PC_HUMAN && score >= TNS_W3 ) - AdjustSync::HandleAutosync( fNoteOffset ); + AdjustSync::HandleAutosync( fNoteOffset, fStepSeconds ); // Do game-specific and mode-specific score mapping. score = GAMESTATE->GetCurrentGame()->MapTapNoteScore( score ); diff --git a/stepmania/src/RageUtil.cpp b/stepmania/src/RageUtil.cpp index 2b7dbfaf96..d3e4b404f5 100644 --- a/stepmania/src/RageUtil.cpp +++ b/stepmania/src/RageUtil.cpp @@ -947,6 +947,57 @@ float calc_stddev( const float *pStart, const float *pEnd, bool bSample ) return fDev; } +bool CalcLeastSquares( const vector< pair > &vCoordinates, + float *pfSlope, float *pfIntercept, float *pfError ) +{ + ASSERT( pfSlope != NULL ); + ASSERT( pfIntercept != NULL ); + int iNumSamples = vCoordinates.size(); + float fNumSamples = static_cast(iNumSamples); + if( iNumSamples == 0 ) + { + return false; + } + float fSumXX = 0.0f, fSumXY = 0.0f, fSumX = 0.0f, fSumY = 0.0f; + for( int i = 0; i < iNumSamples; ++i ) + { + fSumXX += vCoordinates[i].first * vCoordinates[i].first; + fSumXY += vCoordinates[i].first * vCoordinates[i].second; + fSumX += vCoordinates[i].first; + fSumY += vCoordinates[i].second; + } + float fDenominator = fNumSamples * fSumXX - fSumX * fSumX; + *pfSlope = (fNumSamples * fSumXY - fSumX * fSumY) / fDenominator; + *pfIntercept = (fSumXX * fSumY - fSumX * fSumXY) / fDenominator; + + *pfError = 0.0f; + for( int i = 0; i < iNumSamples; ++i ) + { + float fOneError = (vCoordinates[i].second - (*pfIntercept + *pfSlope * vCoordinates[i].first)); + *pfError += fOneError * fOneError; + } + *pfError /= fNumSamples; + *pfError = sqrtf( *pfError ); + return true; +} + +void FilterHighErrorPoints( vector< pair > *vCoordinates, + float fSlope, float fIntercept, float fCutoff ) +{ + unsigned int iOut = 0; + for( unsigned int iIn = 0; iIn < vCoordinates->size(); ++iIn ) + { + float fError = ((*vCoordinates)[iIn].second - + (fIntercept + fSlope * (*vCoordinates)[iIn].first)); + if( fabsf(fError) < fCutoff ) + { + (*vCoordinates)[iOut] = (*vCoordinates)[iIn]; + ++iOut; + } + } + vCoordinates->resize(iOut); +} + void TrimLeft( RString &sStr, const char *s ) { int n = 0; diff --git a/stepmania/src/RageUtil.h b/stepmania/src/RageUtil.h index 810d6d00c4..b59d0ae794 100644 --- a/stepmania/src/RageUtil.h +++ b/stepmania/src/RageUtil.h @@ -431,6 +431,22 @@ float calc_mean( const float *pStart, const float *pEnd ); * deviation. */ float calc_stddev( const float *pStart, const float *pEnd, bool bSample = false ); +/* + * Find the slope, intercept, and error of a linear least squares regression + * of the points given. Error is returned as the sqrt of the average squared + * Y distance from the chosen line. + * Returns true on success, false on failure. + */ +bool CalcLeastSquares( const vector< pair > &vCoordinates, + float *p_fSlope, float *p_fIntercept, float *p_fError ); + +/* + * This method throws away any points that are more than fCutoff away from + * the line defined by fSlope and fIntercept. + */ +void FilterHighErrorPoints( vector< pair > *vCoordinates, + float fSlope, float fIntercept, float fCutoff ); + template int FindIndex( T1 begin, T1 end, const T2 *p ) { diff --git a/stepmania/src/ScreenDebugOverlay.cpp b/stepmania/src/ScreenDebugOverlay.cpp index f8b0d915f8..8e8566bce8 100644 --- a/stepmania/src/ScreenDebugOverlay.cpp +++ b/stepmania/src/ScreenDebugOverlay.cpp @@ -441,6 +441,7 @@ static LocalizedString SLOW ( "ScreenDebugOverlay", "Slow" ); static LocalizedString CPU ( "ScreenDebugOverlay", "CPU" ); static LocalizedString SONG ( "ScreenDebugOverlay", "Song" ); static LocalizedString MACHINE ( "ScreenDebugOverlay", "Machine" ); +static LocalizedString SYNC_TEMPO ( "ScreenDebugOverlay", "Tempo" ); class DebugLineAutoplay : public IDebugLine @@ -497,9 +498,10 @@ class DebugLineAutosync : public IDebugLine { switch( GAMESTATE->m_SongOptions.GetSong().m_AutosyncType ) { - case SongOptions::AUTOSYNC_OFF: return OFF.GetValue(); break; - case SongOptions::AUTOSYNC_SONG: return SONG.GetValue(); break; - case SongOptions::AUTOSYNC_MACHINE: return MACHINE.GetValue(); break; + case SongOptions::AUTOSYNC_OFF: return OFF.GetValue(); break; + case SongOptions::AUTOSYNC_SONG: return SONG.GetValue(); break; + case SongOptions::AUTOSYNC_MACHINE: return MACHINE.GetValue(); break; + case SongOptions::AUTOSYNC_TEMPO: return SYNC_TEMPO.GetValue(); break; default: ASSERT(0); } } @@ -509,7 +511,8 @@ class DebugLineAutosync : public IDebugLine { int as = GAMESTATE->m_SongOptions.GetSong().m_AutosyncType + 1; bool bAllowSongAutosync = !GAMESTATE->IsCourseMode(); - if( !bAllowSongAutosync && as == SongOptions::AUTOSYNC_SONG ) + if( !bAllowSongAutosync && + ( as == SongOptions::AUTOSYNC_SONG || as == SongOptions::AUTOSYNC_TEMPO ) ) as = SongOptions::AUTOSYNC_MACHINE; wrap( as, SongOptions::NUM_AUTOSYNC_TYPES ); SO_GROUP_ASSIGN( GAMESTATE->m_SongOptions, ModsLevel_Song, m_AutosyncType, SongOptions::AutosyncType(as) ); diff --git a/stepmania/src/ScreenEdit.cpp b/stepmania/src/ScreenEdit.cpp index 79024ff11e..26f21518e5 100644 --- a/stepmania/src/ScreenEdit.cpp +++ b/stepmania/src/ScreenEdit.cpp @@ -2122,6 +2122,7 @@ void ScreenEdit::TransitionEditState( EditState em ) break; case STATE_PLAYING: + AdjustSync::HandleSongEnd(); if( AdjustSync::IsSyncDataChanged() ) ScreenSaveSync::PromptSaveSync(); break; diff --git a/stepmania/src/ScreenGameplay.cpp b/stepmania/src/ScreenGameplay.cpp index a632ca40fb..594c9d6276 100644 --- a/stepmania/src/ScreenGameplay.cpp +++ b/stepmania/src/ScreenGameplay.cpp @@ -2245,6 +2245,7 @@ void ScreenGameplay::SaveStats() void ScreenGameplay::SongFinished() { + AdjustSync::HandleSongEnd(); SaveStats(); // Let subclasses save the stats. /* Extremely important: if we don't remove attacks before moving on to the next * screen, they'll still be turned on eventually. */ diff --git a/stepmania/src/ScreenGameplaySyncMachine.cpp b/stepmania/src/ScreenGameplaySyncMachine.cpp index a8c14189ae..5090af17db 100644 --- a/stepmania/src/ScreenGameplaySyncMachine.cpp +++ b/stepmania/src/ScreenGameplaySyncMachine.cpp @@ -94,7 +94,7 @@ void ScreenGameplaySyncMachine::ResetAndRestartCurrentSong() StartPlayingSong( 4, 0 ); // reset autosync - AdjustSync::s_iAutosyncOffsetSample = 0; + AdjustSync::ResetAutosync(); } static LocalizedString OLD_OFFSET ( "ScreenGameplaySyncMachine", "Old offset" ); @@ -110,7 +110,7 @@ void ScreenGameplaySyncMachine::RefreshText() s += OLD_OFFSET.GetValue() + ssprintf( ": %0.3f\n", fOld ); s += NEW_OFFSET.GetValue() + ssprintf( ": %0.3f\n", fNew ); s += STANDARD_DEVIATION.GetValue() + ssprintf( ": %0.3f\n", fStdDev ); - s += COLLECTING_SAMPLE.GetValue() + ssprintf( ": %d / %d", AdjustSync::s_iAutosyncOffsetSample+1, SAMPLE_COUNT ); + s += COLLECTING_SAMPLE.GetValue() + ssprintf( ": %d / %d", AdjustSync::s_iAutosyncOffsetSample+1, AdjustSync::OFFSET_SAMPLE_COUNT ); m_textSyncInfo.SetText( s ); } diff --git a/stepmania/src/ScreenSyncOverlay.cpp b/stepmania/src/ScreenSyncOverlay.cpp index 6befc5d2c0..f16ce3068c 100644 --- a/stepmania/src/ScreenSyncOverlay.cpp +++ b/stepmania/src/ScreenSyncOverlay.cpp @@ -83,6 +83,7 @@ static LocalizedString AUTO_PLAY ( "ScreenSyncOverlay", "AutoPlay" ); static LocalizedString AUTO_PLAY_CPU ( "ScreenSyncOverlay", "AutoPlayCPU" ); static LocalizedString AUTO_SYNC_SONG ( "ScreenSyncOverlay", "AutoSync Song" ); static LocalizedString AUTO_SYNC_MACHINE ( "ScreenSyncOverlay", "AutoSync Machine" ); +static LocalizedString AUTO_SYNC_TEMPO ( "ScreenSyncOverlay", "AutoSync Tempo" ); void ScreenSyncOverlay::UpdateText() { vector vs; @@ -103,6 +104,7 @@ void ScreenSyncOverlay::UpdateText() case SongOptions::AUTOSYNC_OFF: break; case SongOptions::AUTOSYNC_SONG: vs.push_back(AUTO_SYNC_SONG); break; case SongOptions::AUTOSYNC_MACHINE: vs.push_back(AUTO_SYNC_MACHINE); break; + case SongOptions::AUTOSYNC_TEMPO: vs.push_back(AUTO_SYNC_TEMPO); break; default: ASSERT(0); } diff --git a/stepmania/src/SongOptions.cpp b/stepmania/src/SongOptions.cpp index 69e7a7e0bb..33ffbcc724 100644 --- a/stepmania/src/SongOptions.cpp +++ b/stepmania/src/SongOptions.cpp @@ -75,10 +75,11 @@ void SongOptions::GetMods( vector &AddTo ) const switch( m_AutosyncType ) { - case AUTOSYNC_OFF: break; + case AUTOSYNC_OFF: break; case AUTOSYNC_SONG: AddTo.push_back("AutosyncSong"); break; case AUTOSYNC_MACHINE: AddTo.push_back("AutosyncMachine"); break; - default: ASSERT(0); + case AUTOSYNC_TEMPO: AddTo.push_back("AutosyncTempo"); break; + default: ASSERT(0); } } @@ -163,9 +164,11 @@ void SongOptions::FromString( const RString &sOptions ) else if( sBit == "assisttick" ) m_bAssistTick = on; else if( sBit == "autosync" || sBit == "autosyncsong" ) - m_AutosyncType = on ? AUTOSYNC_SONG : AUTOSYNC_OFF; + m_AutosyncType = on ? AUTOSYNC_SONG : AUTOSYNC_OFF; else if( sBit == "autosyncmachine" ) - m_AutosyncType = on ? AUTOSYNC_MACHINE : AUTOSYNC_OFF; + m_AutosyncType = on ? AUTOSYNC_MACHINE : AUTOSYNC_OFF; + else if( sBit == "autosynctempo" ) + m_AutosyncType = on ? AUTOSYNC_TEMPO : AUTOSYNC_OFF; else if( sBit == "savescore" ) m_bSaveScore = on; else if( sBit == "bar" ) m_LifeType = LIFE_BAR; else if( sBit == "battery" ) m_LifeType = LIFE_BATTERY; diff --git a/stepmania/src/SongOptions.h b/stepmania/src/SongOptions.h index 3cae7c46a9..62156cd057 100644 --- a/stepmania/src/SongOptions.h +++ b/stepmania/src/SongOptions.h @@ -33,6 +33,7 @@ public: AUTOSYNC_OFF, AUTOSYNC_SONG, AUTOSYNC_MACHINE, + AUTOSYNC_TEMPO, NUM_AUTOSYNC_TYPES }; AutosyncType m_AutosyncType;