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.
 
You could just use the complete lyric near the top, save it as a txt file and take it from there

If I try it I want to see it working to my satisfaction. Which means I want a music file and a text file with lyrics to match it. :) I made one for myself, though.

Anyhow, here we go. I didn't do anything fancy, but I tried a few different methods. The first thing I picked up on in seriously looking at this, is that there is no real need for timing code, because TMediaPlayer will do that for us by telling us where in the file it is. Which means the matter goes to the lyrics and the delay loop.

To start out, I didn't use time stamps as described in Post #1, I used millisecond offsets (mainly to save time going through and changing it by hand in my data file), but converting the timestamps to MS is not a worrisome matter to accomplish.

It all appeared to work satisfactorily to me, but I'm sure there might be something somewhere that has been missed. Take it as something thrown together more than something polished.

Method #1 - TTimer
Here's the original question. I'll post most of the source here than in the others, since it repeats itself.

Code:
type
  LRecord = record
    offset: DWord;
    LyricLine: String;
  end;
var
  Form1: TForm1;
  LyricRecord: array[1..100] of LRecord;
  { hopefully there aren't songs this long!}
  LyricCount: integer;
  LyricPos : integer;

implementation

{$R *.DFM}

procedure LoadLyricFile(LFile: string);
  { loads the lyric file into the data structures above.
    Done this way to front-load processing time and allow
    seeking around easier }
  var
    LTextfile: Text;
    LTextString: String;
    RPos: integer;
  begin
    assign(LTextFile, LFile);
    Reset(LTextFile);
    LyricCount := 1;
    while not eof(LTextFile) do
      begin
        readln(LTextFile, LTextString);
        RPos := Pos(']', LTextString);
        LyricRecord[LyricCount].LyricLine :=
                Copy(LTextString, RPos+1, Length(LTextString)-RPos);
        LyricRecord[LyricCount].Offset :=
                StrToInt(Copy(LtextString, 2, RPos-2));
        inc(lyriccount);
      end;
    { to signal the logic we're done with the data }
    LyricRecord[LyricCount].Offset := 999999999;
    close(LTextFile);
  end;

procedure TForm1.Button1Click(Sender: TObject);
{ loads the music file and lyrics file.  Sets controls for
  initial run }
begin
  if OpenDialog1.Execute then
    begin
      MediaPlayer1.FileName := OpenDialog1.FileName;
      MediaPlayer1.Open;
      TrackBar1.Max := MediaPlayer1.TrackLength[1];
      TrackBar1.Position := 0;
      Button2.Enabled := true;
      Form1.Caption := OpenDialog1.Filename + ' loaded.';
      Label1.Caption := IntToStr(MediaPlayer1.Position) + ' of '
                       + IntToStr(MediaPlayer1.Tracklength[1]);
      LoadLyricFile('lyric.txt');
      LyricPos := 1;
    end;
end;

procedure TForm1.Button2Click(Sender: TObject);
{ plays the music file }
begin
  MediaPlayer1.Play;
  TrackBar1.Enabled := false;
  Timer1.Enabled := true;
  Button2.Enabled := false;
  Button3.Enabled := true;
end;

procedure TForm1.Button3Click(Sender: TObject);
{ stops the music file at the current position }
begin
  MediaPlayer1.Stop;
  Timer1.Enabled := false;
  Button2.Enabled := true;
  TrackBar1.Enabled := true;
  Button3.Enabled := false;
end;

Now the interesting stuff:

Code:
procedure TForm1.Timer1Timer(Sender: TObject);
{ the TTimerevent code - Timer1 is set to run every 10 ms,
  the check is made to see whether the MediaPlayer has
  gone beyond the current watch lyric - if it has, then
  display it }
begin
  TrackBar1.Position := MediaPlayer1.Position;
  Label1.Caption := IntToStr(MediaPlayer1.Position) + ' of '
                    + IntToStr(MediaPlayer1.Tracklength[1]);
  if MediaPlayer1.Position >= LyricRecord[LyricPos].Offset then
    begin
      Edit1.Text := LyricRecord[LyricPos].LyricLine;
      inc(LyricPos);
    end;
  if MediaPlayer1.Position = MediaPlayer1.TrackLength[1] then
    begin
      Button2.Enabled := true;
      Button3.Enabled := false;
      Timer1.Enabled := false;
      LyricPos := 1;
    end;
end;

procedure TForm1.TrackBar1Change(Sender: TObject);
{ I have a trackbar on the form which enables changing the
  offset of the song when it is not playing.  This sets the 
  Mediaplayer and then seeks through the Lyric data to find 
  the proper position.  A lyric will not show with this 
  code until the next event, but the current one can easily 
  be placed on the Edit  control I am using }
begin
  MediaPlayer1.Position := TrackBar1.Position;
  LyricPos := 1;
  while LyricRecord[LyricPos].Offset < MediaPlayer1.Position do
    inc(LyricPos);
end;

Method #2 - Application.ProcessMessages
I'm not posting complete code here, since it is (mostly) the same. Of major significance is that I have a global boolean (Playing) defined. The timer control is gone, and consequently the timer event code as well. The code that was in the timer event is what is run with the UpdateStuff; call.

One great illustration of my statement earlier about 90% of things being solvable with A.P.

Code:
procedure TForm1.Button2Click(Sender: TObject);
begin
  MediaPlayer1.Play;
  Playing := true;
  TrackBar1.Enabled := false;
  Button2.Enabled := false;
  Button3.Enabled := true;
  while Playing do
    begin
      sleep(10);
      UpdateStuff;
      Application.ProcessMessages;
    end;
end;

When the button to stop the music is pressed, Playing is set to false, and the while loop is broken.

Method #3 - BeginThread
Here's what I came up with. Again, a great illustration of complexity issues that I referred to.

In the form definition, we need:
Code:
  protected
     procedure UpdateStuff(var WinMsg: TMessage); message WM_USER+1;
  end;

Inter-process (thread/app) communication can be accomplished via the use of messages (think like interrupts if you have dealt with DOS/ASM). In Windows, you can define your own messages in your app to respond to (like DLL to main), or you can substitute or add to the messages already in use (like the Delphi VCL does when you define an event).

screen_id is a DWord global variable which defines the thread handle.

Code of interest:
Code:
procedure Tform1.ScreenThread;
  { the thread procedure - again basic loop }
  begin
    While Playing do
      begin
        sleep(10);
        SendMessage(Form1.handle, WM_USER+1, 0, 0);
      end;
    { probably could/should stick EndThread(0) here }
  end;

procedure TForm1.UpdateStuff(var WinMsg: TMessage);
{ updates the controls on screen }
  begin
    TrackBar1.Position := MediaPlayer1.Position;
    Label1.Caption := IntToStr(MediaPlayer1.Position) + ' of '
                    + IntToStr(MediaPlayer1.Tracklength[1]);
    if MediaPlayer1.Position >= LyricRecord[LyricPos].Offset then
      begin
        Edit1.Text := LyricRecord[LyricPos].LyricLine;
        inc(LyricPos);
      end;
    if MediaPlayer1.Position = MediaPlayer1.TrackLength[1] then
      begin
        Button2.Enabled := true;
        Button3.Enabled := false;
        LyricPos := 1;
      end;
  end;

procedure TForm1.Button2Click(Sender: TObject);
{ play the file }
begin
  MediaPlayer1.Play;
  Playing := true;
  TrackBar1.Enabled := false;
  Button2.Enabled := false;
  Button3.Enabled := true;
  BeginThread(nil, 0, Addr(TForm1.ScreenThread), nil, 0, screen_id);
  { not sure there's a need to wait on anything here }
end;
 
Cheers Glenn, you deserve a star [smile]. Now I just have to figure it all out, and will report back in a day or two.
 
After I have loaded a media file (button1), it displays an error dialog box stating File not found. The following line is highlighted in this section:

procedure LoadLyricFile(LFile: string);
{ loads the lyric file into the data structures above.
Done this way to front-load processing time and allow
seeking around easier }
var
LTextfile: Text;
LTextString: String;
RPos: integer;
begin
assign(LTextFile, LFile);
Reset(LTextFile);
LyricCount := 1;
while not eof(LTextFile) do
begin
readln(LTextFile, LTextString);
RPos := Pos(']', LTextString);
LyricRecord[LyricCount].LyricLine :=
Copy(LTextString, RPos+1, Length(LTextString)-RPos);
LyricRecord[LyricCount].Offset :=
StrToInt(Copy(LtextString, 2, RPos-2));
inc(lyriccount);
end;
{ to signal the logic we're done with the data }
LyricRecord[LyricCount].Offset := 999999999;
close(LTextFile);
end;
 
You have to set the file to a location that it will find. The current directory is the directory the music file is in that you select in the opendialog.

I had a thought: if it would help, I could post a link to a file with both the projects in it. Would that help?
 
You have to set the file to a location that it will find. The current directory is the directory the music file is in that you select in the opendialog.
That explains it [smile]
I had a thought: if it would help, I could post a link to a file with both the projects in it. Would that help?
It would be a very big help, thank you. I find that I learn much better by practical examples, rather than theory.
 
It would be a very big help, thank you. I find that I learn much better by practical examples, rather than theory.

Here you are. These were compiled under Delphi 3 and work there. I don't know how farther ahead they work.
 
I don't know how farther ahead they work

Many thanks! The code seems to compile without any errors.

Now I'm not sure if I'm missing something somewhere. I've loaded your projects and run them, picked a file (both mp3 and wav) and then clicked Play. No sound, no lyrics. The trackbar doesn't appear to move, although a static number appears on the label caption. If I make the MediaPlayer visible, and click the Play button on the navigator, then the track plays and there is sound. Still no lyrics tho'. I have even modified the lyric file location, no success.
 
Nice examples there Glenn, start for you!
And thanks for assisting me in this long thread! [spin]

One remark though :

Keep your UI code clean.
Now you may think why on on earth would I do that, Delphi is known for RAD development; drop a few components on the form, attach a few event handlers to it and it works! Yes this is true, but this will work only with very small projects.

I will tell you a story from my own personal experience.

I started up my own company with my day work buddy in '95.
We needed a program that could make invoices. I made the database and it's fronted in ms access. We used that interface for about 5 years. As our company growed, we hired some people to do the dirty work :) Soon it became clear that the access frontend was lacking some features and it also became slow. I suggested to rewrite the frontend in Delphi. As I didn't have the time to do this, we asked my buddy's brother (a COBOL programmer with Delphi experience) to do the job. And he did a fantastic job, the program did everything we wanted! As our ms access database became bigger in size, it began to crash (we had 5 simultaneous users working at that time). After numerous data corruptions I was fed up with access, I decided we needed a new db infrastructure. Since we didn't have the $$$ to buy ms SQL, there was that nice open source alternative called MySQL. We asked my buddy's brother to port the program to MySQL (which wasn't very difficult because we used ADO components in the program, only a few queries needed to be rewritten due to differences between access and MySQL) and everything went smooth again. Over the years we hired more and more employees (we have 10 in total now) and the due to changes in our business, some parts of the program were no longer needed and other parts needed to change. Due to a high workload, my buddy's brother didn't have much time to work on the program, and that's when I decided to take over his task. And this is where my nightmare begins. Imagine an application with more than 40 different forms (each having their own unit). The problem that I have today, if I want to change a part of the program, let's say one form, I spend a few days trying to figure out what each event was doing/ following the business logic. for example, on that particular form, there is a 'Save' button. the business logic is written in the eventhandler of that button. problem is, that this procedure is about 3 pages long, dealing with 10+ queries and interacts with UI elements on the form. I spend for about a week refactoring the form's code, the unit has now half the code size, but the business logic is still in the event handlers. In a second phase I will need to create a separate class that deals with the business logic (queries and calculations) so I can isolate the presentation layer (= User Interface) from the Bussiness Layer. Why is this so important?

- Unit tests, I can test my bussiness logic without any problem

- What if tommorow I want to have the UI as a web page? If things are isolated, I can keep the bussiness logic and I only need to write a new presentation layer.

This is my point that I made at the top of this thread: My TThread implementation may seem like overkill, but it is one single class containing all the logic.

Something about myself:
I dont have a degree in informatics. I learned everything I know from books and self-learning. I still have a day job (Telecom), where I am a full time delphi programmer at this moment, but more and more customers are asking for .NET skills. This means I also need to work on some C# projects. Luckily I have some collegues that are very skilled in that area, and they learned me how to use code patterns. (look here for more info : I am applying the same principles to my delphi projects and they do save me a lot of time :)

I you guys are still reading this, then I hope it wasn't a boring read (and I need to level up up my english writing skills (it's not my native language [spin]))

Now I need a drink! [morning]

Cheers,
/Daddy

-----------------------------------------------------
What You See Is What You Get
Never underestimate tha powah of tha google!
 
Now I'm not sure if I'm missing something somewhere. I've loaded your projects and run them, picked a file (both mp3 and wav) and then clicked Play. No sound, no lyrics.

I loaded it into TD2006 and found that everytime the Position variable is changed in code, OnChange is fired, effectively grinding the system to a halt. This is not the case with Delphi 3, OnChange seems to only fire when the user changes it.

Comment out the TrackBarChange code and you should see it work. I guess, unless I'm missing something, it would require some UI changes to fix. (Perhaps I should bite the bullet with the obscenely slow load times of the development environment and start converting everything :( )
 
I dont have a degree in informatics. I learned everything I know from books and self-learning.

Really, that is the case with anyone, degree or not. It's rare for someone to have any proficiency coming into anything from classes alone. (unfortunately most don't realize that)

Coming from that background of having some formal training,
I'll say the only real advantage I found in it was that I have some awareness of some logic and debugging tools which are not readily arrived upon by trial and error. And yes, they have been useful to the point of solving problems many seem to struggle to grasp that don't have the background.

Most of what you described are things I'm already aware of. But a good lesson and a reminder, nonetheless, for those that do not know (star coming for it). I guess we learn a number of things through our experiences. As I've mentioned, one of those is the value of simplicity.

After looking at programs that are convoluted in design with thousands of lines of source that take weeks to make sense of, its tended to deepen my appreciation of things that are not needlessly complex. But as your story indicates, there is often an evolution of need (case in point: I spent some time isolating a number of functions out of my Delphi stuff because I noticed they appeared repeatedly in programs).

Ultimately, with the amount of data and evolution in technology, it always comes back to self-learning and finding the right books (one thing that the formal training helps in too). Of course, the issues of time, effort, and will comes into play with that one (I think I'm up to 7 or 8 things I haven't looked through yet).
 
Comment out the TrackBarChange code and you should see it work.
Yes, it does. Thank you.

In the examples you provided, you uses a different timestamp/timings format in the lyric file:
[16952]Born down in a dead man's town
[20586]The first kick I took was when I hit the ground
[24851]You end up like a dog that's been beat too much
[28157]Till you spend half your life just covering up
[32692]Born in the U.S.A.
[36381]I was born in the U.S.A.
[40574]I was born in the U.S.A.
[44355]Born in the U.S.A.
[49430]Got in a little hometown jam so they put a rifle in my hand
[56869]Sent me off to a foreign land to go and kill the yellow man
[64748]Born in the U.S.A.
compared to mine:
[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.

How does your timings work?
 
Ok, I figured it out [smile]. Correct me if I'm wrong.

The first 2 figures are the minutes in seconds, i.e.
is 1 min plus 4 secs = 64
is 2 mins plus 30 secs = 150

If it is correct, it leads to my next question.

How did you obtain those timings? I assume you created a project, loaded the track in MediaPlayer and whenever you pressed a button, it gave you the timings?

Would you care to share that code too [wink] Thank you!
 
I did a quick experiment to obtain the current position in a track:

procedure TForm1.Button2Click(Sender: TObject);
var
Time : Integer;
begin
MediaPlayer1.TimeFormat := tfHMS;
Time := MediaPlayer1.Position;
Label1.Caption := inttostr(time);
end;

I'm not too sure if that is correct, or if tfHMS is the right one to use.
 
How did you obtain those timings?

Would you care to share that code too Thank you!

You have the code already. If you notice, label1 puts those timings out to screen. The timings are indeed in milliseconds (the default).
 
me said:
Comment out the TrackBarChange code and you should see it work. I guess, unless I'm missing something, it would require some UI changes to fix.

I was missing something. This will fix it in the second version:

Code:
procedure TForm1.UpdateStuff(var WinMsg: TMessage);
  begin
    TrackBar1.OnChange := nil;
    Trackbar1.Position := MediaPlayer1.Position;
    TrackBar1.OnChange := TrackBar1Change;
    Label1.Caption := IntToStr(MediaPlayer1.Position) + ' of ' +
                      IntToStr(MediaPlayer1.Length);
    if MediaPlayer1.Position >= LyricRecord[LyricPos].Offset then
      begin
        Edit1.Text := LyricRecord[LyricPos].LyricLine;
        inc(LyricPos);
      end;
    if MediaPlayer1.Position = MediaPlayer1.Length then
      begin
        Button2.Enabled := true;
        Button3.Enabled := false;
        LyricPos := 1;
        Playing := false;
      end;
  end;

 
Glenn9999
I was missing something. This will fix it in the second version:

Thank you! This is really great.

Going back to the timings code:
var
Time : Integer;
begin
MediaPlayer1.TimeFormat := tfHMS;
Time := MediaPlayer1.Position;
Label1.Caption := inttostr(time);
end;

It displays the time as

I've been trying to display it in the mm:ss:mm (Minutes, Seconds, Milliseconds) format for another application, and I'm stumped. I've tried all sorts of things like TimeFormat, StrToTime, and somehow can't seem to hack it. Is there an easy way of achieving this?
 
More on the soapbox: One thing about all these components is that it can cloud one into not seeing a solution.

Doing the math, etc.

Code:
function TForm1.mstotime(inms: longint): string;
  var
    minute, second, ms: longint;
  begin
    minute := (inms div 1000) div 60;
    second := (inms div 1000) mod 60;
    ms := inms mod 1000;
    Result := IntToStr(minute) + ':' + IntToStr(second) + ':' + InttoStr(ms);
  end;
 
More on the soapbox: One thing about all these components is that it can cloud one into not seeing a solution.

Thank you, and I agree. Knew I had to divide by 1000 but hit a blank after that.

Think I'm going to watch the Masters and give this a rest.
 
Glenn, I'm trying to do some error trapping in your code, and can't seem to nail it down.

When I open a track and lyric file without any timestamps and try to play it through media player, it throws up an exception:

" is not a valid integer value

The code I use for playing is taken from your code:

Code:
procedure LoadLyricFile(LFile: string);
  var
    LTextfile: Text;
    LTextString: String;
    RPos: integer;
  begin
    assign(LTextFile, LFile);
    Reset(LTextFile);
    LyricCount := 1;
    while not eof(LTextFile) do
      begin
        readln(LTextFile, LTextString);
        RPos := Pos(']', LTextString);
        LyricRecord[LyricCount].LyricLine :=
                Copy(LTextString, RPos+1, Length(LTextString)-RPos);
        LyricRecord[LyricCount].Offset :=
                StrToInt(Copy(LtextString, 2, RPos-2));
        inc(lyriccount);
      end;
    { to signal the logic we're done with the data }
    LyricRecord[LyricCount].Offset := 999999999;
    close(LTextFile);
  end;

procedure TMediaPlayerForm.Btn_PlayTrackClick(Sender: TObject);
begin
btn_playtrack.Enabled:=false;
btn_stoptrack.Enabled:=true;
LoadLyricFile('lyricstest.txt');
LyricPos := 1;
MediaPlayer1.Rewind;
mediaplayer1.Play;
Timer_TrackPlayer.Enabled:=true;
btn_stoptrack.SetFocus;
end;

procedure TMediaPlayerForm.Timer_TrackPlayerTimer(Sender: TObject);
begin
if MediaPlayer1.Position >= LyricRecord[LyricPos].Offset then
    begin
      Label_Lyrics.Caption:= LyricRecord[LyricPos].LyricLine;
      inc(LyricPos);
    end;
  if MediaPlayer1.Position = MediaPlayer1.TrackLength[1] then
    begin
      Timer_TrackPlayer.Enabled := false;
      LyricPos := 1;
end;
end;

The exception appears on this code in the playtrack section:
Code:
LyricPos := 1;

What I am trying to do is that if a lyric file is loaded without any timestamps or incomplete, that the display of lyrics is disabled but the track keeps playing. I just can't figure out where it actually does the checking.
 
Okay, what was typed was more quick and dirty than anything. As far as something production-oriented, you usually want to check for things. The exception is in the LoadLyricFile function. Here's how you'd test for a conversion problem in such a thing:

Code:
try
  LyricRecord[LyricCount].Offset :=
                StrToInt(Copy(LtextString, 2, RPos-2));
except
  On E: EConvertError do
    begin
      ShowMessage('Offset values are not numeric, aborting lyric load.');
      exit;
    end;
end;

You can also check for if RPos > 0, which indicates that a ']' is present within the file. Hopefully that'll help.

----------
Those who work hard are rewarded with more work and remembered come time to downsize. Those who hardly work are given a paycheck and ignored completely.
 
Status
Not open for further replies.

Part and Inventory Search

Sponsor

Back
Top