diff options
-rw-r--r-- | src/base/UBeatTimer.pas | 159 | ||||
-rw-r--r-- | src/base/UIni.pas | 22 | ||||
-rw-r--r-- | src/base/UMusic.pas | 4 | ||||
-rw-r--r-- | src/base/UTime.pas | 158 | ||||
-rw-r--r-- | src/media/UAudioPlaybackBase.pas | 1 | ||||
-rw-r--r-- | src/media/UMedia_dummy.pas | 1 | ||||
-rw-r--r-- | src/menu/UMenuBackgroundVideo.pas | 1 | ||||
-rw-r--r-- | src/screens/UScreenSing.pas | 32 |
8 files changed, 293 insertions, 85 deletions
diff --git a/src/base/UBeatTimer.pas b/src/base/UBeatTimer.pas index 310a49cd..bc03de76 100644 --- a/src/base/UBeatTimer.pas +++ b/src/base/UBeatTimer.pas @@ -43,7 +43,15 @@ type *) TLyricsState = class private - Timer: TRelativeTimer; // keeps track of the current time + fTimer: TRelativeTimer; // keeps track of the current time + fSyncSource: TSyncSource; + fAvgSyncDiff: real; + fLastClock: real; // last master clock value + // Note: do not use Timer.GetState() to check if lyrics are paused as + // Timer.Pause() is used for synching. + fPaused: boolean; + + function Synchronize(LyricTime: real): real; public OldBeat: integer; // previous discovered beat CurrentBeat: integer; // current beat (rounded) @@ -68,28 +76,61 @@ type TotalTime: real; // total song time constructor Create(); - procedure Pause(); - procedure Resume(); + {** + * Resets the LyricsState state. + *} procedure Reset(); + procedure UpdateBeats(); + {** + * Sets a master clock for this LyricsState. If no sync-source is set + * or SyncSource is nil the internal timer is used. + *} + procedure SetSyncSource(SyncSource: TSyncSource); + + {** + * Starts the timer. This is either done + * - immediately if WaitForTrigger is false or + * - after the first call to GetCurrentTime()/SetCurrentTime() or Start(false) + *} + procedure Start(WaitForTrigger: boolean = false); + + {** + * Pauses the timer. + * The counter is preserved and can be resumed by a call to Start(). + *} + procedure Pause(); + + {** + * Stops the timer. + * The counter is reset to 0. + *} + procedure Stop(); + (** - * current song time (in seconds) used as base-timer for lyrics etc. + * Returns/Sets the current song time (in seconds) used as base-timer for lyrics etc. + * If GetCurrentTime()/SetCurrentTime() if Start() was called *) function GetCurrentTime(): real; procedure SetCurrentTime(Time: real); end; implementation -uses UNote, Math; + +uses + UNote, + ULog, + SysUtils, + Math; constructor TLyricsState.Create(); begin // create a triggered timer, so we can Pause() it, set the time // and Resume() it afterwards for better synching. - Timer := TRelativeTimer.Create(true); + fTimer := TRelativeTimer.Create(); // reset state Reset(); @@ -97,24 +138,110 @@ end; procedure TLyricsState.Pause(); begin - Timer.Pause(); + fTimer.Pause(); + fPaused := true; end; -procedure TLyricsState.Resume(); +procedure TLyricsState.Start(WaitForTrigger: boolean); begin - Timer.Resume(); + fTimer.Start(WaitForTrigger); + fPaused := false; + fLastClock := -1; + fAvgSyncDiff := -1; +end; + +procedure TLyricsState.Stop(); +begin + fTimer.Stop(); + fPaused := false; end; procedure TLyricsState.SetCurrentTime(Time: real); begin - // do not start the timer (if not started already), - // after setting the current time - Timer.SetTime(Time, false); + fTimer.SetTime(Time); + fLastClock := -1; + fAvgSyncDiff := -1; end; +{.$DEFINE LOG_SYNC} + +function TLyricsState.Synchronize(LyricTime: real): real; +var + MasterClock: real; + TimeDiff: real; +const + AVG_HISTORY_FACTOR = 0.7;
+ PAUSE_THRESHOLD = 0.010; // 10ms
+ FORWARD_THRESHOLD = 0.010; // 10ms
+begin + MasterClock := fSyncSource.GetClock();
+ Result := LyricTime;
+
+ // do not sync if lyrics are paused externally or if the timestamp is old
+ if (fPaused or (MasterClock = fLastClock)) then
+ Exit; + + // calculate average time difference (some sort of weighted mean). + // The bigger AVG_HISTORY_FACTOR is, the smoother is the average diff.
+ // This is done as some timestamps might be wrong or even lower
+ // than their predecessor.
+ TimeDiff := MasterClock - LyricTime;
+ if (fAvgSyncDiff = -1) then
+ fAvgSyncDiff := TimeDiff
+ else
+ fAvgSyncDiff := TimeDiff * (1-AVG_HISTORY_FACTOR) +
+ fAvgSyncDiff * AVG_HISTORY_FACTOR;
+
+ {$IFDEF LOG_SYNC}
+ //Log.LogError(Format('TimeDiff: %.3f', [TimeDiff]));
+ {$ENDIF}
+
+ // do not go backwards in time as this could mess up the score + if (fAvgSyncDiff > FORWARD_THRESHOLD) then + begin + {$IFDEF LOG_SYNC} + Log.LogError('Sync: ' + floatToStr(MasterClock) + ' > ' + floatToStr(LyricTime));
+ {$ENDIF}
+ + Result := LyricTime + fAvgSyncDiff; + fTimer.SetTime(Result); + fTimer.Start(); + fAvgSyncDiff := -1; + end + else if (fAvgSyncDiff < -PAUSE_THRESHOLD) then + begin + // wait until timer and master clock are in sync (> 10ms) + fTimer.Pause(); + + {$IFDEF LOG_SYNC} + Log.LogError('Pause: ' + floatToStr(MasterClock) + ' < ' + floatToStr(LyricTime)); + {$ENDIF} + end + else if (fTimer.GetState = rtsPaused) and (fAvgSyncDiff >= 0) then + begin + fTimer.Start();
+ + {$IFDEF LOG_SYNC} + Log.LogError('Unpause: ' + floatToStr(LyricTime)); + {$ENDIF} + end; + fLastClock := MasterClock; +end;
+ function TLyricsState.GetCurrentTime(): real; +var + LyricTime: real; +begin + LyricTime := fTimer.GetTime(); + if Assigned(fSyncSource) then + Result := Synchronize(LyricTime) + else + Result := LyricTime; +end; + +procedure TLyricsState.SetSyncSource(SyncSource: TSyncSource); begin - Result := Timer.GetTime(); + fSyncSource := SyncSource;
end; (** @@ -124,8 +251,10 @@ end; *) procedure TLyricsState.Reset(); begin - Pause(); - SetCurrentTime(0); + Stop(); + fPaused := false; + + fSyncSource := nil; StartTime := 0; TotalTime := 0; diff --git a/src/base/UIni.pas b/src/base/UIni.pas index 6b93d7ba..d809c790 100644 --- a/src/base/UIni.pas +++ b/src/base/UIni.pas @@ -127,6 +127,8 @@ type AudioOutputBufferSizeIndex: integer; VoicePassthrough: integer; + SyncTo: integer; + //Song Preview PreviewVolume: integer; PreviewFading: integer; @@ -218,6 +220,12 @@ const IVoicePassthrough: array[0..1] of UTF8String = ('Off', 'On'); +const + ISyncTo: array[0..2] of UTF8String = ('Music', 'Lyrics', 'Off'); +type + TSyncToType = (stMusic, stLyrics, stOff); + +const IAudioOutputBufferSize: array[0..9] of UTF8String = ('Auto', '256', '512', '1024', '2048', '4096', '8192', '16384', '32768', '65536'); IAudioOutputBufferSizeVals: array[0..9] of integer = ( 0, 256, 512 , 1024 , 2048 , 4096 , 8192 , 16384 , 32768 , 65536 ); @@ -290,6 +298,8 @@ var IVoicePassthroughTranslated: array[0..1] of UTF8String = ('Off', 'On'); + ISyncToTranslated: array[0..2] of UTF8String = ('Music', 'Lyrics', 'Off'); + //Song Preview IPreviewVolumeTranslated: array[0..10] of UTF8String = ('Off', '10%', '20%', '30%', '40%', '50%', '60%', '70%', '80%', '90%', '100%'); @@ -413,6 +423,10 @@ begin IVoicePassthroughTranslated[0] := ULanguage.Language.Translate('OPTION_VALUE_OFF'); IVoicePassthroughTranslated[1] := ULanguage.Language.Translate('OPTION_VALUE_ON'); + ISyncToTranslated[Ord(stMusic)] := ULanguage.Language.Translate('OPTION_VALUE_MUSIC'); + ISyncToTranslated[Ord(stLyrics)] := ULanguage.Language.Translate('OPTION_VALUE_LYRICS'); + ISyncToTranslated[Ord(stOff)] := ULanguage.Language.Translate('OPTION_VALUE_OFF'); + ILyricsFontTranslated[0] := ULanguage.Language.Translate('OPTION_VALUE_PLAIN'); ILyricsFontTranslated[1] := ULanguage.Language.Translate('OPTION_VALUE_OLINE1'); ILyricsFontTranslated[2] := ULanguage.Language.Translate('OPTION_VALUE_OLINE2'); @@ -881,7 +895,7 @@ begin TabsAtStartup := Tabs; //Tabs at Startup fix // Song Sorting - Sorting := GetArrayIndex(ISorting, IniFile.ReadString('Game', 'Sorting', ISorting[0])); + Sorting := GetArrayIndex(ISorting, IniFile.ReadString('Game', 'Sorting', ISorting[Ord(sEdition)])); // Debug Debug := GetArrayIndex(IDebug, IniFile.ReadString('Game', 'Debug', IDebug[0])); @@ -974,6 +988,9 @@ begin // PartyPopup PartyPopup := GetArrayIndex(IPartyPopup, IniFile.ReadString('Advanced', 'PartyPopup', 'On')); + // SyncTo + SyncTo := GetArrayIndex(ISyncTo, IniFile.ReadString('Advanced', 'SyncTo', ISyncTo[Ord(stMusic)])); + // Joypad Joypad := GetArrayIndex(IJoypad, IniFile.ReadString('Controller', 'Joypad', IJoypad[0])); @@ -1115,6 +1132,9 @@ begin //Party Popup IniFile.WriteString('Advanced', 'PartyPopup', IPartyPopup[PartyPopup]); + //SyncTo + IniFile.WriteString('Advanced', 'SyncTo', ISyncTo[SyncTo]); + // Joypad IniFile.WriteString('Controller', 'Joypad', IJoypad[Joypad]); diff --git a/src/base/UMusic.pas b/src/base/UMusic.pas index 03d20740..7f2b3e30 100644 --- a/src/base/UMusic.pas +++ b/src/base/UMusic.pas @@ -188,10 +188,6 @@ type end; type - TSyncSource = class - function GetClock(): real; virtual; abstract; - end; - TAudioProcessingStream = class; TOnCloseHandler = procedure(Stream: TAudioProcessingStream); diff --git a/src/base/UTime.pas b/src/base/UTime.pas index 83844cb5..0610ef59 100644 --- a/src/base/UTime.pas +++ b/src/base/UTime.pas @@ -40,20 +40,26 @@ type function GetTime(): real; end; + TRelativeTimerState = (rtsStopped, rtsWait, rtsPaused, rtsRunning); + TRelativeTimer = class private AbsoluteTime: int64; // system-clock reference time for calculation of CurrentTime - RelativeTimeOffset: real; - Paused: boolean; + RelativeTime: real; TriggerMode: boolean; + State: TRelativeTimerState; public - constructor Create(TriggerMode: boolean = false); + constructor Create(); + procedure Start(WaitForTrigger: boolean = false); procedure Pause(); - procedure Resume(); + procedure Stop(); function GetTime(): real; - function GetAndResetTime(): real; - procedure SetTime(Time: real; Trigger: boolean = true); - procedure Reset(); + procedure SetTime(Time: real); + function GetState(): TRelativeTimerState; + end; + + TSyncSource = class + function GetClock(): real; virtual; abstract; end; procedure CountSkipTimeSet; @@ -126,85 +132,115 @@ end; * TRelativeTimer **} -(* - * creates a new timer. - * if triggermode is false (default), the timer - * will immediately begin with counting. - * if triggermode is true, it will wait until get/settime() or pause() is called - * for the first time. +(** + * Creates a new relative timer. + * A relative timer works like a stop-watch. It can be paused and + * resumed afterwards, continuing with the counter it had when it was paused. *) -constructor TRelativeTimer.Create(TriggerMode: boolean); +constructor TRelativeTimer.Create(); begin - inherited Create(); - Self.TriggerMode := TriggerMode; - Reset(); - Paused := false; + State := rtsStopped; + AbsoluteTime := 0; + RelativeTime := 0; end; -procedure TRelativeTimer.Pause(); +(** + * Starts the timer. + * If WaitForTrigger is false the timer will be started immediately. + * If WaitForTrigger is true the timer will be started when a trigger event + * occurs. A trigger event is a call of one of the Get-/SetTime() methods. + * In addition the timer can be started by calling this method again with + * WaitForTrigger set to false. + *) +procedure TRelativeTimer.Start(WaitForTrigger: boolean = false); begin - RelativeTimeOffset := GetTime(); - Paused := true; + case (State) of + rtsStopped, rtsPaused: begin + if (WaitForTrigger) then + begin + State := rtsWait; + end + else + begin + State := rtsRunning; + AbsoluteTime := SDL_GetTicks(); + end; + end; + + rtsWait: begin + if (not WaitForTrigger) then + begin + State := rtsRunning; + AbsoluteTime := SDL_GetTicks(); + RelativeTime := 0; + end; + end; + end; end; -procedure TRelativeTimer.Resume(); +(** + * Pauses the timer and leaves the counter untouched. + *) +procedure TRelativeTimer.Pause(); begin - AbsoluteTime := SDL_GetTicks(); - Paused := false; + if (State = rtsRunning) then + begin + // Important: GetTime() must be called in running state + RelativeTime := GetTime(); + State := rtsPaused; + end; end; -(* - * Returns the counter of the timer. - * If in TriggerMode it will return 0 and start the counter on the first call. +(** + * Stops the timer and sets its counter to 0. *) -function TRelativeTimer.GetTime: real; +procedure TRelativeTimer.Stop(); begin - // initialize absolute time on first call in triggered mode - if (TriggerMode and (AbsoluteTime = 0)) then + if (State <> rtsStopped) then begin - AbsoluteTime := SDL_GetTicks(); - Result := RelativeTimeOffset; - Exit; + State := rtsStopped; + RelativeTime := 0; end; - - if Paused then - Result := RelativeTimeOffset - else - Result := RelativeTimeOffset + (SDL_GetTicks() - AbsoluteTime) / cSDLCorrectionRatio; end; -(* - * Returns the counter of the timer and resets the counter to 0 afterwards. - * Note: In TriggerMode the counter will not be stopped as with Reset(). +(** + * Returns the current counter of the timer. + * If WaitForTrigger was true in Start() the timer will be started + * if it was not already running. *) -function TRelativeTimer.GetAndResetTime(): real; +function TRelativeTimer.GetTime(): real; begin - Result := GetTime(); - SetTime(0); + case (State) of + rtsStopped, rtsPaused: + Result := RelativeTime; + rtsRunning: + Result := RelativeTime + (SDL_GetTicks() - AbsoluteTime) / cSDLCorrectionRatio; + rtsWait: begin + // start triggered + State := rtsRunning; + AbsoluteTime := SDL_GetTicks(); + Result := RelativeTime; + end; + end; end; -(* - * Sets the timer to the given time. This will trigger in TriggerMode if - * Trigger is set to true. Otherwise the counter's state will not change. +(** + * Sets the counter of the timer. + * If WaitForTrigger was true in Start() the timer will be started + * if it was not already running. *) -procedure TRelativeTimer.SetTime(Time: real; Trigger: boolean); +procedure TRelativeTimer.SetTime(Time: real); begin - RelativeTimeOffset := Time; - if ((not TriggerMode) or Trigger) then - AbsoluteTime := SDL_GetTicks(); + RelativeTime := Time; + AbsoluteTime := SDL_GetTicks(); + // start triggered + if (State = rtsWait) then + State := rtsRunning; end; -(* - * Resets the counter of the timer to 0. - * If in TriggerMode the timer will not start counting until it is triggered again. - *) -procedure TRelativeTimer.Reset(); +function TRelativeTimer.GetState(): TRelativeTimerState; begin - RelativeTimeOffset := 0; - if (TriggerMode) then - AbsoluteTime := 0 - else - AbsoluteTime := SDL_GetTicks(); + Result := State; end; end. diff --git a/src/media/UAudioPlaybackBase.pas b/src/media/UAudioPlaybackBase.pas index de2d5563..5f317257 100644 --- a/src/media/UAudioPlaybackBase.pas +++ b/src/media/UAudioPlaybackBase.pas @@ -35,6 +35,7 @@ interface uses UMusic, + UTime, UPath; type diff --git a/src/media/UMedia_dummy.pas b/src/media/UMedia_dummy.pas index 35b8bd70..8ebfd3a9 100644 --- a/src/media/UMedia_dummy.pas +++ b/src/media/UMedia_dummy.pas @@ -38,6 +38,7 @@ implementation uses SysUtils, math, + UTime, UMusic, UPath; diff --git a/src/menu/UMenuBackgroundVideo.pas b/src/menu/UMenuBackgroundVideo.pas index 006c45e0..bfaee702 100644 --- a/src/menu/UMenuBackgroundVideo.pas +++ b/src/menu/UMenuBackgroundVideo.pas @@ -131,6 +131,7 @@ begin if (fBgVideo <> nil) then begin VideoBGTimer.SetTime(0); + VideoBGTimer.Start(); fBgVideo.Loop := true; fBgVideo.Play; end; diff --git a/src/screens/UScreenSing.pas b/src/screens/UScreenSing.pas index 269ef201..e4764760 100644 --- a/src/screens/UScreenSing.pas +++ b/src/screens/UScreenSing.pas @@ -58,6 +58,10 @@ type function GetClock(): real; override; end; + TMusicSyncSource = class(TSyncSource) + function GetClock(): real; override; + end; + type TScreenSing = class(TMenu) private @@ -65,6 +69,7 @@ type fCurrentVideo: IVideo; fVideoClip: IVideo; fLyricsSync: TLyricsSyncSource; + fMusicSync: TMusicSyncSource; protected eSongLoaded: THookableEvent; //< event is called after lyrics of a song are loaded on OnShow Paused: boolean; //pause Mod @@ -256,7 +261,7 @@ begin end else // disable pause begin - LyricsState.Resume(); + LyricsState.Start(); // play music AudioPlayback.Play; @@ -325,6 +330,7 @@ begin Theme.LyricBar.LowerX, Theme.LyricBar.LowerY, Theme.LyricBar.LowerW, Theme.LyricBar.LowerH); fLyricsSync := TLyricsSyncSource.Create(); + fMusicSync := TMusicSyncSource.Create(); eSongLoaded := THookableEvent.Create('ScreenSing.SongLoaded'); @@ -659,11 +665,21 @@ begin AudioPlayback.Open(CurrentSong.Path.Append(CurrentSong.Mp3)); AudioPlayback.SetVolume(1.0); AudioPlayback.Position := CurrentSong.Start; - // synchronize music to the lyrics - AudioPlayback.SetSyncSource(fLyricsSync); + + // synchronize music + if (Ini.SyncTo = Ord(stLyrics)) then + AudioPlayback.SetSyncSource(fLyricsSync) + else + AudioPlayback.SetSyncSource(nil); + + // synchronize lyrics (do not set this before AudioPlayback is initialized) + if (Ini.SyncTo = Ord(stMusic)) then + LyricsState.SetSyncSource(fMusicSync) + else + LyricsState.SetSyncSource(nil); // start lyrics - LyricsState.Resume(); + LyricsState.Start(true); // start music AudioPlayback.Play(); @@ -907,6 +923,9 @@ begin AudioPlayback.Stop; AudioPlayback.SetSyncSource(nil); + LyricsState.Stop(); + LyricsState.SetSyncSource(nil); + // close video files fVideoClip := nil; fCurrentVideo := nil; @@ -1045,5 +1064,10 @@ begin Result := LyricsState.GetCurrentTime(); end; +function TMusicSyncSource.GetClock(): real; +begin + Result := AudioPlayback.Position; +end; + end. |