From edfc692c991e08af0163aa6812e5972478d7191b Mon Sep 17 00:00:00 2001
From: tobigun <tobigun@b956fd51-792f-4845-bead-9b4dfca2ff2c>
Date: Thu, 22 Apr 2010 01:04:24 +0000
Subject: - now it is possible to sync lyrics to audio - ini option SyncTo
 added - lyric to audio is default now (instead of sync audio to lyrics) -
 modified RelativeTimer (hopefully easier to use and more self-explanatory)

git-svn-id: svn://svn.code.sf.net/p/ultrastardx/svn/trunk@2273 b956fd51-792f-4845-bead-9b4dfca2ff2c
---
 src/base/UBeatTimer.pas           | 159 ++++++++++++++++++++++++++++++++++----
 src/base/UIni.pas                 |  22 +++++-
 src/base/UMusic.pas               |   4 -
 src/base/UTime.pas                | 158 ++++++++++++++++++++++---------------
 src/media/UAudioPlaybackBase.pas  |   1 +
 src/media/UMedia_dummy.pas        |   1 +
 src/menu/UMenuBackgroundVideo.pas |   1 +
 src/screens/UScreenSing.pas       |  32 +++++++-
 8 files changed, 293 insertions(+), 85 deletions(-)

(limited to 'src')

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.
 
-- 
cgit v1.2.3