aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/base/UBeatTimer.pas159
-rw-r--r--src/base/UIni.pas22
-rw-r--r--src/base/UMusic.pas4
-rw-r--r--src/base/UTime.pas158
-rw-r--r--src/media/UAudioPlaybackBase.pas1
-rw-r--r--src/media/UMedia_dummy.pas1
-rw-r--r--src/menu/UMenuBackgroundVideo.pas1
-rw-r--r--src/screens/UScreenSing.pas32
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.