From 37f9178f61a7421d584b7f2a6008c42cff9b5bdb Mon Sep 17 00:00:00 2001 From: John Bauer Date: Thu, 26 Oct 2006 22:41:40 +0000 Subject: [PATCH] Add an "Autosync Tempo" feature. This uses a linear Least Squares Regression to figure out a line through the user's step errors to minimize that error. It then uses the slope of that line to fix the tempo and the intercept to fix the offset. Stops are adjusted as if they were originally calculated as a number of beats, ie the slope is also used to change the stops. Also fixed is a bug where the user doesn't get a chance to keep or reject the sync changes when ESCing (ZZing) from a song. R=glenn --- stepmania/Themes/default/Languages/de.ini | 4 +- stepmania/Themes/default/Languages/en.ini | 15 +- stepmania/Themes/default/Languages/es.ini | 4 +- stepmania/Themes/default/Languages/fr.ini | 4 +- stepmania/Themes/default/Languages/it.ini | 4 +- stepmania/Themes/default/metrics.ini | 3 +- stepmania/src/AdjustSync.cpp | 215 +++++++++++++++++--- stepmania/src/AdjustSync.h | 36 +++- stepmania/src/GameState.cpp | 4 +- stepmania/src/Player.cpp | 10 +- stepmania/src/RageUtil.cpp | 51 +++++ stepmania/src/RageUtil.h | 16 ++ stepmania/src/ScreenDebugOverlay.cpp | 11 +- stepmania/src/ScreenEdit.cpp | 1 + stepmania/src/ScreenGameplay.cpp | 1 + stepmania/src/ScreenGameplaySyncMachine.cpp | 4 +- stepmania/src/ScreenSyncOverlay.cpp | 2 + stepmania/src/SongOptions.cpp | 11 +- stepmania/src/SongOptions.h | 1 + 19 files changed, 338 insertions(+), 59 deletions(-) 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;