Tek-Tips is the largest IT community on the Internet today!

Members share and learn making Tek-Tips Forums the best source of peer-reviewed technical information on the Internet!

  • Congratulations biv343 on being selected by the Tek-Tips community for having the most helpful posts in the forums last week. Way to Go!

TTimer in Delphi to display synced lyrics? 6

Status
Not open for further replies.

doctorjellybean

Programmer
May 5, 2003
145
There is a clever program called MiniLyrics, and I want to achieve the same effect in my multimedia application.

That is, when it plays a track, it loads the lyrics from a file and display it synchronized to the track. MiniLyrics uses timestamps in its file, e.g.

[00:07.79]Jennifer Juniper lives upon the hill,
[00:15.23]Jennifer Juniper, sitting very still.
[00:23.36]Is she sleeping ? I don't think so.
[00:26.55]Is she breathing ? Yes, very low.


Is it possible to achieve the same object with the TTimer component, and if yes, what is the best way?

Thank you.
 
Start song, record current time.

Set the TTimer.Interval to 10 (every one hundredeth of a second).

Attach event to OnTimer. Current time - start time = song time stamp. If next lyric line should be shown, show it.

Not sure how much CPU time this approach will use. A better one would be to adjust the TTimer.Interval to the appropriate gap between the current lyric and the next lyric, and just blindly display the next lyric each time the TTimer.OnTimer event fires. This approach will slowly get out of sync though unless you add some check code. Maybe a combination of the two I've mentioned will be sufficient.

You may find that the OnTimer is not accurate enough - I'm not sure. I only say that because of the multitude of 'high-performance' timers out there. Check out the JEDI project for example ( It's a confusing mess, but once installed all sorts of gems can be found.
 
While using TTimer is certainly not bad, I tend to use threads in this case.
The great thing about this, is that you can make a skeleton class and use it for all kinds of things...
I'm making an example and will post this tommorow :)

/Daddy

-----------------------------------------------------
What You See Is What You Get
Never underestimate tha powah of tha google!
 
It's been a while since I've messed with MCI but I think this can be retrieved with MediaPlayer.OnNotify event through Win API. See Win32 Developers Reference starting at MCI_TMSF_FRAME:

"The MCI_TMSF_FRAME macro retrieves the frames component from a parameter containing packed tracks/minutes/seconds/frames (TMSF) information."

See also, MCIWndGetPositionString: "Returns an integer corresponding to the current position. The units for the position value depend on the current time format."

I wrote an mp3 app once that tracked time played / time remaining. (It was before embedded lyrics were available.) I'm not sure that's the exact call I used; I'll have to dig out my old code but I'm sure it would be more efficient than syncing an additional timer.


Roo
Delphi Rules!
 
For resolution sake, I don't think any more than millisecond resolution is necessary in this case. As far as timing code goes, I do have something more involved worked out (from help on here in fact) I can post. But the simplest is this:

Code:
function WinMSSinceStart: DWord;
          stdcall; external 'winmm.dll' name 'timeGetTime';

It turns a number of Milliseconds since the multi-media timer is initialized (I guess?). Calling it is simple:

Code:
Result := WinMSSinceStart;

Load all your timestamps and lyrics into an array, converting them to MS values, and then before you start the song, call this. Then in a loop, call the routine and then check each entry as it comes up in your table - if the time elapsed (currenttime-startingtime) is greater or equal to the lyric time, then display the lyric and move on to the next entry.
 
Glenn9999, do you think you could perhaps post the complete code, taking the above lyrics as an example? It would really be a great help :) I could then copy and paste it into Delphi (5) and experiment with it.

Thanks in advance.
 
ok, doctorjellybean
here is the deal:

start a new form application project,
put a memo on the form and name it Memo
put a button on the form and name it Btn_play

I saved your lyrics in a file named 'test.txt'.

the provided code is tested and works as intended, but it is far from complete (for example pause functionality could be added and so on...). Maybe it is overkill for what you want to achieve but as I said, this sort of class can be used in a lot of cases.

Code:
unit u_frm_main;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, u_class_lyrics;

type
  TFrm_main = class(TForm)
    Memo: TMemo;
    Btn_play: TButton;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Btn_playClick(Sender: TObject);
  private
    { Private declarations }
    LyricsThread : TLyricsThread;
    procedure OnLyric(Sender : TObject);
    procedure OnPlayEnd(Sender : TObject);
  public
    { Public declarations }
  end;

var
  Frm_main: TFrm_main;

implementation

{$R *.dfm}

procedure TFrm_main.OnLyric(Sender: TObject);

var Lyric : TLyric;

begin
 Lyric := TLyric(Sender);
 Memo.Lines.Add(FormatDateTime('NN:SS.ZZ - ', Lyric.PlayTime) +  Lyric.Lyric)
end;

procedure TFrm_main.OnPlayEnd(Sender: TObject);
begin
 Memo.Lines.Add('playing stopped');
end;

procedure TFrm_main.Btn_playClick(Sender: TObject);
begin
 Memo.Clear;
 LyricsThread.Play('test.txt', 10);
end;

procedure TFrm_main.FormCreate(Sender: TObject);
begin
 LyricsThread := TLyricsThread.Create;
 LyricsThread.OnLyric := OnLyric;
 LyricsThread.OnPlayEnd := OnPlayEnd;
 LyricsThread.Resume;
end;

procedure TFrm_main.FormDestroy(Sender: TObject);
begin
 LyricsThread.OnLyric := nil;
 LyricsThread.OnPlayEnd := nil;
 LyricsThread.Terminate;
 WaitForSingleObject(LyricsThread.ThreadDone, 3000);
 FreeAndNil(LyricsThread);
end;

end.

and now the class where the real work is done:
save it as u_class_lyrics.pas

Code:
unit u_class_lyrics;

interface

uses
  Windows, Classes, SysUtils, Contnrs, DateUtils;

type
  TLyric = class
    PlayTime : TDateTime;
    Lyric    : string;
  end;

  TLyricsThread = class(TThread)
  private
    FLyrics  : TObjectList;
    FPlayList : TObjectList;
    FOnLyric : TNotifyEvent;
    FStarted : Boolean;
    FStartTime : TDateTime;
    FStartOffset : Integer;
    FLyricIndex : Integer;
    FOnPlayEnd: TNotifyEvent;
    procedure SyncOnLyric;
    procedure SyncOnPlayEnd;
    procedure DoOnLyric;
    procedure DoOnPlayEnd;
    procedure DoWork;
    procedure LoadLyricsFile(Filename : String);
    procedure LoadPlayList(Offset : Integer);
  protected
    procedure Execute; override;
  public
    ThreadDone : THandle;
    constructor Create;
    destructor Destroy; override;
    procedure Play(LyricsFile : String; Offset : Integer);
  published
    property OnLyric : TNotifyEvent read FOnLyric write FOnLyric;
    property OnPlayEnd : TNotifyEvent read FOnPlayEnd write FOnPlayEnd;
  end;

implementation

{ TLyricsThread }
procedure TLyricsThread.SyncOnLyric;
begin
 if Assigned(FOnLyric) then
  Synchronize(DoOnLyric);
 Inc(FLyricIndex);
end;

procedure TLyricsThread.SyncOnPlayEnd;
begin
 if Assigned(FOnPlayEnd) then
  Synchronize(DoOnPlayEnd);
end;

procedure TLyricsThread.DoOnLyric;
begin
 FOnLyric(TLyric(FPlayList[FLyricIndex]));
end;

procedure TLyricsThread.DoOnPlayEnd;
begin
 FOnPlayEnd(Self);
end;

procedure TLyricsThread.LoadLyricsFile(Filename: string);

var Index   : Integer;
    StrList : TStringList;
    Line    : string;
    FLyric  : TLyric;
    FormatSettings : TFormatSettings;

begin
 // this function assumes that the file is indeed a lyrics file with the correct format
 FLyrics.Clear;
 if FileExists(Filename) then
  begin
   StrList := TStringList.Create;
   GetLocaleFormatSettings(LOCALE_SYSTEM_DEFAULT, FormatSettings);
   FormatSettings.TimeSeparator := ':';
   FormatSettings.DecimalSeparator := '.';
   try
    StrList.LoadFromFile(FileName);
    Index := StrList.Count;
    while Index > 0 do begin
     Dec(Index);
     Line := Trim(StrList[Index]);
     if Line <> '' then begin
      FLyric := TLyric.Create;
      FLyric.PlayTime := StrToTime('00:' + Copy(Line, Pos('[', Line) + 1, Pos(']', Line) - Pos('[', Line) - 1),
                                   FormatSettings);
      FLyric.Lyric := Copy(Line, Pos(']', Line) + 1, MaxInt);
      // add to our list, when the file is completely loaded the first Lyric has index(0)
      FLyrics.Insert(0, FLyric);
     end;
    end;
   finally
    FreeAndNil(StrList);
   end;
  end;
end;

procedure TLyricsThread.LoadPlayList(Offset: Integer);

var Index : Integer;
    Lyric : TLyric;

begin
 // load only the lyrics we need to play
 FPlayList.Clear;
 Index := FLyrics.Count;
 while Index > 0 do
  begin
   Dec(Index);
   Lyric := TLyric(FLyrics[Index]);
   if Lyric.PlayTime >= EncodeTime(0, 0, Offset, 0) then
    FPlayList.Insert(0, Lyric);
  end;
end;

procedure TLyricsThread.Play(LyricsFile : String; Offset: Integer);
begin
 // play our lyrics file, with an eventual offset in seconds
 LoadLyricsFile(LyricsFile);
 LoadPlayList(Offset);
 FStartOffset := OffSet;
 FStartTime := Now;
 FLyricIndex := 0;
 FStarted := True;
end;

procedure TLyricsThread.DoWork;

var Lyric : TLyric;
    T : TDateTime;

begin
 if FStarted then
  begin
   if FLyricIndex > (FPlayList.Count - 1) then
    begin
     FStarted := False;
     FLyricIndex := 0;
     SyncOnPlayEnd;
    end
   else
    begin
     Lyric := TLyric(FPlayList[FLyricIndex]);
     T := IncSecond(Now - FStartTime, FStartOffset);
     if T >= Lyric.PlayTime then
      SyncOnLyric;
    end;
  end;
end;

// main thread loop
procedure TLyricsThread.Execute;
begin
 while not Terminated do
  begin
   Sleep(10); // 10 millisecond resolution should be enough
   DoWork;
  end;
 SetEvent(ThreadDone);
end;


constructor TLyricsThread.Create;
begin
 inherited Create(True);
 FreeOnTerminate := False;
 FStarted := False;
 // create objects
 FLyrics := TObjectList.Create(True);
 FPlayList := TObjectList.Create(False);
 ThreadDone := CreateEvent(nil, True, False, nil);
end;

destructor TLyricsThread.Destroy;
begin
 // destroy objects
 CloseHandle(ThreadDone);
 FreeAndNil(FLyrics);
 FreeAndNil(FPlayList);
 inherited;
end;

end.

Cheers,
Daddy

-----------------------------------------------------
What You See Is What You Get
Never underestimate tha powah of tha google!
 
doctorjellybean,

does this example helps you out or not? some reaction is always appreciated [wink]

-----------------------------------------------------
What You See Is What You Get
Never underestimate tha powah of tha google!
 
Hi Daddy

You must have read my mind [bigsmile] I was just about to post when the post notification arrived in my inbox.

Yes, a big thank you. It looks very complicated, way above my knowledge but I'll try it out and report back [smile]

 
Ok Daddy, here goes. When I try to run the project, I get the following error messages:

[Error] u_class_lyrics.pas(75): Undeclared identifier: 'TFormatSettings'
[Error] u_class_lyrics.pas(83): Undeclared identifier: 'GetLocaleFormatSettings'
[Error] u_class_lyrics.pas(84): Missing operator or semicolon
[Error] u_class_lyrics.pas(85): Missing operator or semicolon
[Error] u_class_lyrics.pas(95): Too many actual parameters
[Fatal Error] u_frm_main.pas(7): Could not compile used unit 'u_class_lyrics.pas'

I'm using Delphi 6 by the way.
 
ok,

try to comment the formatsettings lines and remove the Formatsettings parameter from the TimeToStr function.
only important thing is that the decimal separator a '.' is so StrToTime can work with the provided time string.

Cheers,
Daddy

-----------------------------------------------------
What You See Is What You Get
Never underestimate tha powah of tha google!
 
This is the amended section of u_class_lyrics.pas

procedure TLyricsThread.LoadLyricsFile(Filename: string);

var Index : Integer;
StrList : TStringList;
Line : string;
FLyric : TLyric;
//FormatSettings : TFormatSettings;

begin
// this function assumes that the file is indeed a lyrics file with the correct format
FLyrics.Clear;
if FileExists(Filename) then
begin
StrList := TStringList.Create;
//GetLocaleFormatSettings(LOCALE_SYSTEM_DEFAULT, FormatSettings);
//FormatSettings.TimeSeparator := ':';
//FormatSettings.DecimalSeparator := '.';
try
StrList.LoadFromFile(FileName);
Index := StrList.Count;
while Index > 0 do begin
Dec(Index);
Line := Trim(StrList[Index]);
if Line <> '' then begin
FLyric := TLyric.Create;
FLyric.PlayTime := StrToTime('00:' + Copy(Line, Pos('[', Line) + 1, Pos(']', Line) - Pos('[', Line) - 1));
FLyric.Lyric := Copy(Line, Pos(']', Line) + 1, MaxInt);
// add to our list, when the file is completely loaded the first Lyric has index(0)
FLyrics.Insert(0, FLyric);
end;
end;
finally
FreeAndNil(StrList);
end;
end;
end;

When I run it and click the button, it raises an access exception, with the following line highlighted:

FLyrics.Clear;

In your original post you said the code was tested and worked as provided. Which version of Delphi was that?

Thanks again.
 
I just had another look at the code and at the lyric txt file. The complete lyric txt file is as follow:

[00:07.79]Jennifer Juniper lives upon the hill,
[00:15.23]Jennifer Juniper, sitting very still.
[00:23.36]Is she sleeping ? I don't think so.
[00:26.55]Is she breathing ? Yes, very low.
[00:30.36]Whatcha doing, Jennifer, my love ?
[00:37.43]Jennifer Juniper, rides a dappled mare,
[00:44.81]Jennifer Juniper, lilacs in her hair.
[00:52.55]Is she dreaming ? Yes, I think so.
[00:56.24]Is she pretty ? Yes, ever so.
[01:00.05]Whatcha doing, Jennifer, my love ?
[01:06.30]I'm thinking of what it would be like if she loved me.
[01:14.43]You know just lately this happy song it came along
[01:19.99]And I like to somehow try and tell you.
[01:24.12]Jennifer Juniper, hair of golden flax.
[01:31.43]Jennifer Juniper longs for what she lacks.
[01:39.37]Do you like her ? Yes, I do, Sir.
[01:43.00]Would you love her ? Yes, I would, Sir.
[01:46.93]Whatcha doing Jennifer, my love ?
[01:58.26]Jennifer Juniper, Jennifer Juniper, Jennifer Juniper.
[02:08.50]Jennifer Juniper vit sur la colline,
[02:16.44]Jennifer Juniper assise trÿFFFFE8s tranquille.
[02:19.94]Dort-elle ? Je ne crois pas.
[02:23.69]Respire-t-elle ? Oui, mais tout bas.
[02:27.82]Qu'est-ce que tu fais, Jenny mon amour ?

The timestamps format is minutes:seconds:milliseconds

The code
StrToTime('00:' + Copy(Line, Pos('[', Line) + 1, Pos(']', Line) - Pos('[', Line) - 1));
doesn't appear to make provision for minutes?
 
When I run it and click the button, it raises an access exception, with the following line highlighted:
FLyrics.Clear

simple, the FLyrics object is not initialized, you really need the whole class / unit

The code

Quote:
StrToTime('00:' + Copy(Line, Pos('[', Line) + 1, Pos(']', Line) - Pos('[', Line) - 1));
doesn't appear to make provision for minutes?

that line of code just copies the time part out of the brackets. so take the first lyric:

[00:07.79]Jennifer Juniper lives upon the hill,

resulting string will be 00:00:07.79

StrToTime really needs a hour part, or else it wont work
This code runs under D2006, but should also run under D6

/Daddy



-----------------------------------------------------
What You See Is What You Get
Never underestimate tha powah of tha google!
 
simple, the FLyrics object is not initialized, you really need the whole class / unit

I'm using the whole class / unit, as shown below. I only quoted the part earlier which got changed as per your suggestion.

unit u_class_lyrics;

interface

uses
Windows, Classes, SysUtils, Contnrs, DateUtils;

type
TLyric = class
PlayTime : TDateTime;
Lyric : string;
end;

TLyricsThread = class(TThread)
private
FLyrics : TObjectList;
FPlayList : TObjectList;
FOnLyric : TNotifyEvent;
FStarted : Boolean;
FStartTime : TDateTime;
FStartOffset : Integer;
FLyricIndex : Integer;
FOnPlayEnd: TNotifyEvent;
procedure SyncOnLyric;
procedure SyncOnPlayEnd;
procedure DoOnLyric;
procedure DoOnPlayEnd;
procedure DoWork;
procedure LoadLyricsFile(Filename : String);
procedure LoadPlayList(Offset : Integer);
protected
procedure Execute; override;
public
ThreadDone : THandle;
constructor Create;
destructor Destroy; override;
procedure Play(LyricsFile : String; Offset : Integer);
published
property OnLyric : TNotifyEvent read FOnLyric write FOnLyric;
property OnPlayEnd : TNotifyEvent read FOnPlayEnd write FOnPlayEnd;
end;

implementation

{ TLyricsThread }
procedure TLyricsThread.SyncOnLyric;
begin
if Assigned(FOnLyric) then
Synchronize(DoOnLyric);
Inc(FLyricIndex);
end;

procedure TLyricsThread.SyncOnPlayEnd;
begin
if Assigned(FOnPlayEnd) then
Synchronize(DoOnPlayEnd);
end;

procedure TLyricsThread.DoOnLyric;
begin
FOnLyric(TLyric(FPlayList[FLyricIndex]));
end;

procedure TLyricsThread.DoOnPlayEnd;
begin
FOnPlayEnd(Self);
end;

procedure TLyricsThread.LoadLyricsFile(Filename: string);

var Index : Integer;
StrList : TStringList;
Line : string;
FLyric : TLyric;
//FormatSettings : TFormatSettings;

begin
// this function assumes that the file is indeed a lyrics file with the correct format
FLyrics.Clear;
if FileExists(Filename) then
begin
StrList := TStringList.Create;
//GetLocaleFormatSettings(LOCALE_SYSTEM_DEFAULT, FormatSettings);
//FormatSettings.TimeSeparator := ':';
//FormatSettings.DecimalSeparator := '.';
try
StrList.LoadFromFile(FileName);
Index := StrList.Count;
while Index > 0 do begin
Dec(Index);
Line := Trim(StrList[Index]);
if Line <> '' then begin
FLyric := TLyric.Create;
FLyric.PlayTime := StrToTime('00:' + Copy(Line, Pos('[', Line) + 1, Pos(']', Line) - Pos('[', Line) - 1));
FLyric.Lyric := Copy(Line, Pos(']', Line) + 1, MaxInt);
// add to our list, when the file is completely loaded the first Lyric has index(0)
FLyrics.Insert(0, FLyric);
end;
end;
finally
FreeAndNil(StrList);
end;
end;
end;

procedure TLyricsThread.LoadPlayList(Offset: Integer);

var Index : Integer;
Lyric : TLyric;

begin
// load only the lyrics we need to play
FPlayList.Clear;
Index := FLyrics.Count;
while Index > 0 do
begin
Dec(Index);
Lyric := TLyric(FLyrics[Index]);
if Lyric.PlayTime >= EncodeTime(0, 0, Offset, 0) then
FPlayList.Insert(0, Lyric);
end;
end;

procedure TLyricsThread.Play(LyricsFile : String; Offset: Integer);
begin
// play our lyrics file, with an eventual offset in seconds
LoadLyricsFile(LyricsFile);
LoadPlayList(Offset);
FStartOffset := OffSet;
FStartTime := Now;
FLyricIndex := 0;
FStarted := True;
end;

procedure TLyricsThread.DoWork;

var Lyric : TLyric;
T : TDateTime;

begin
if FStarted then
begin
if FLyricIndex > (FPlayList.Count - 1) then
begin
FStarted := False;
FLyricIndex := 0;
SyncOnPlayEnd;
end
else
begin
Lyric := TLyric(FPlayList[FLyricIndex]);
T := IncSecond(Now - FStartTime, FStartOffset);
if T >= Lyric.PlayTime then
SyncOnLyric;
end;
end;
end;

// main thread loop
procedure TLyricsThread.Execute;
begin
while not Terminated do
begin
Sleep(10); // 10 millisecond resolution should be enough
DoWork;
end;
SetEvent(ThreadDone);
end;


constructor TLyricsThread.Create;
begin
inherited Create(True);
FreeOnTerminate := False;
FStarted := False;
// create objects
FLyrics := TObjectList.Create(True);
FPlayList := TObjectList.Create(False);
ThreadDone := CreateEvent(nil, True, False, nil);
end;

destructor TLyricsThread.Destroy;
begin
// destroy objects
CloseHandle(ThreadDone);
FreeAndNil(FLyrics);
FreeAndNil(FPlayList);
inherited;
end;

end.

Screenshot of resulting error:

Thanks for the StrToTime explanation, it makes sense.
 
I suppose the u_frm_main code is the same as I posted?

the Play method will call the Loadlyrics method.

are you sure the onCreate and onDestroy methods of your form are linked?

/Daddy


-----------------------------------------------------
What You See Is What You Get
Never underestimate tha powah of tha google!
 
I suppose the u_frm_main code is the same as I posted?

Yes it is

are you sure the onCreate and onDestroy methods of your form are linked?

Oops! I forgot [mad] It works now that has been done!


Just 2 things: (1) how can I display it without the timestamps, and (2) that it only display one line at a time? So that it clears the old line before displaying the new one.

Many thanks, it is brilliant [smile]
 
Status
Not open for further replies.

Part and Inventory Search

Sponsor

Back
Top