{* UltraStar Deluxe - Karaoke Game
*
* UltraStar Deluxe is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; see the file COPYING. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*
* $URL$
* $Id$
*}
unit ULyrics;
interface
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
{$I switches.inc}
uses
gl,
glext,
UTexture,
UThemes,
UMusic;
type
// stores two textures for enabled/disabled states
TPlayerIconTex = array [0..1] of TTexture;
TLyricsEffect = (lfxSimple, lfxZoom, lfxSlide, lfxBall, lfxShift);
PLyricWord = ^TLyricWord;
TLyricWord = record
X: real; // left corner
Width: real; // width
Start: cardinal; // start of the word in quarters (beats)
Length: cardinal; // length of the word in quarters
Text: UTF8String; // text
Freestyle: boolean; // is freestyle?
end;
TLyricWordArray = array of TLyricWord;
TLyricLine = class
public
Text: UTF8String; // text
Width: real; // width
Height: real; // height
Words: TLyricWordArray; // words in this line
CurWord: integer; // current active word idx (only valid if line is active)
Start: integer; // start of this line in quarters (Note: negative start values are possible due to gap)
StartNote: integer; // start of the first note of this line in quarters
Length: integer; // length in quarters (from start of first to the end of the last note)
Players: byte; // players that should sing that line (bitset, Player1: 1, Player2: 2, Player3: 4)
LastLine: boolean; // is this the last line of the song?
constructor Create();
destructor Destroy(); override;
procedure Reset();
end;
TLyricEngine = class
private
LastDrawBeat: real;
UpperLine: TLyricLine; // first line displayed (top)
LowerLine: TLyricLine; // second lind displayed (bottom)
QueueLine: TLyricLine; // third line (will be displayed when lower line is finished)
IndicatorTex: TTexture; // texture for lyric indikator
BallTex: TTexture; // texture of the ball for the lyric effect
QueueFull: boolean; // set to true if the queue is full and a line will be replaced with the next AddLine
LCounter: integer; // line counter
// duet mode - textures for player icons
// FIXME: do not use a fixed player count, use MAX_PLAYERS instead
PlayerIconTex: array[0..5] of TPlayerIconTex;
// Some helper procedures for lyric drawing
procedure DrawLyrics (Beat: real);
procedure UpdateLineMetrics(LyricLine: TLyricLine);
procedure DrawLyricsWords(LyricLine: TLyricLine; X, Y: real; StartWord, EndWord: integer);
procedure DrawLyricsLine(X, W, Y, H: real; Line: TLyricLine; Beat: real);
procedure DrawPlayerIcon(Player: byte; Enabled: boolean; X, Y: real; Size, Alpha: real);
procedure DrawBall(XBall, YBall, Alpha: real);
public
// positions, line specific settings
UpperLineX: real; // X start-pos of UpperLine
UpperLineW: real; // Width of UpperLine with icon(s) and text
UpperLineY: real; // Y start-pos of UpperLine
UpperLineH: real; // Max. font-size of lyrics text in UpperLine
LowerLineX: real; // X start-pos of LowerLine
LowerLineW: real; // Width of LowerLine with icon(s) and text
LowerLineY: real; // Y start-pos of LowerLine
LowerLineH: real; // Max. font-size of lyrics text in LowerLine
// display propertys
LineColor_en: TRGBA; // Color of words in an enabled line
LineColor_dis: TRGBA; // Color of words in a disabled line
LineColor_act: TRGBA; // Color of the active word
FontStyle: byte; // Font for the lyric text
{ // currently not used
FadeInEffect: byte; // Effect for line fading in: 0: No Effect; 1: Fade Effect; 2: Move Upwards from Bottom to Pos
FadeOutEffect: byte; // Effect for line fading out: 0: No Effect; 1: Fade Effect; 2: Move Upwards
}
// song specific settings
BPM: real;
Resolution: integer;
// properties to easily read options of this class
property IsQueueFull: boolean read QueueFull; // line in queue?
property LineCounter: integer read LCounter; // lines that were progressed so far (after last clear)
procedure AddLine(Line: PLine); // adds a line to the queue, if there is space
procedure Draw (Beat: real); // draw the current (active at beat) lyrics
// clears all cached song specific information
procedure Clear(cBPM: real = 0; cResolution: integer = 0);
function GetUpperLine(): TLyricLine;
function GetLowerLine(): TLyricLine;
function GetUpperLineIndex(): integer;
constructor Create(ULX, ULY, ULW, ULH, LLX, LLY, LLW, LLH: real);
procedure LoadTextures;
destructor Destroy; override;
end;
implementation
uses
SysUtils,
USkins,
TextGL,
UGraphic,
UDisplay,
ULog,
math,
UIni;
{ TLyricLine }
constructor TLyricLine.Create();
begin
inherited;
Reset();
end;
destructor TLyricLine.Destroy();
begin
SetLength(Words, 0);
inherited;
end;
procedure TLyricLine.Reset();
begin
Start := 0;
StartNote := 0;
Length := 0;
LastLine := False;
Text := '';
Width := 0;
// duet mode: players of that line (default: all)
Players := $FF;
SetLength(Words, 0);
CurWord := -1;
end;
{ TLyricEngine }
{**
* Initializes the engine.
*}
constructor TLyricEngine.Create(ULX, ULY, ULW, ULH, LLX, LLY, LLW, LLH: real);
begin
inherited Create();
BPM := 0;
Resolution := 0;
LCounter := 0;
QueueFull := False;
UpperLine := TLyricLine.Create;
LowerLine := TLyricLine.Create;
QueueLine := TLyricLine.Create;
LastDrawBeat := 0;
UpperLineX := ULX;
UpperLineW := ULW;
UpperLineY := ULY;
UpperLineH := ULH;
LowerLineX := LLX;
LowerLineW := LLW;
LowerLineY := LLY;
LowerLineH := LLH;
LoadTextures;
end;
{**
* Frees memory.
*}
destructor TLyricEngine.Destroy;
begin
UpperLine.Free;
LowerLine.Free;
QueueLine.Free;
inherited;
end;
{**
* Clears all cached Song specific Information.
*}
procedure TLyricEngine.Clear(cBPM: real; cResolution: integer);
begin
BPM := cBPM;
Resolution := cResolution;
LCounter := 0;
QueueFull := False;
LastDrawBeat:=0;
end;
{**
* Loads textures needed for the drawing the lyrics,
* player icons, a ball for the ball effect and the lyric indicator.
*}
procedure TLyricEngine.LoadTextures;
var
I: Integer;
begin
// lyric indicator (bar that indicates when the line start)
IndicatorTex := Texture.LoadTexture(Skin.GetTextureFileName('LyricHelpBar'), TEXTURE_TYPE_TRANSPARENT, $FF00FF);
// ball for current word hover in ball effect
BallTex := Texture.LoadTexture(Skin.GetTextureFileName('Ball'), TEXTURE_TYPE_TRANSPARENT, 0);
// duet mode: load player icon
for I := 0 to 5 do
begin
PlayerIconTex[I][0] := Texture.LoadTexture(Skin.GetTextureFileName('LyricIcon_P' + InttoStr(I+1)), TEXTURE_TYPE_TRANSPARENT, 0);
PlayerIconTex[I][1] := Texture.LoadTexture(Skin.GetTextureFileName('LyricIconD_P' + InttoStr(I+1)), TEXTURE_TYPE_TRANSPARENT, 0);
end;
end;
{**
* Adds LyricLine to queue.
* The LyricEngine stores three lines in its queue:
* UpperLine: the upper line displayed in the lyrics
* LowerLine: the lower line displayed in the lyrics
* QueueLine: an offscreen line that precedes LowerLine
* If the queue is full the next call to AddLine will replace UpperLine with
* LowerLine, LowerLine with QueueLine and QueueLine with the Line parameter.
*}
procedure TLyricEngine.AddLine(Line: PLine);
var
LyricLine: TLyricLine;
I: integer;
begin
// only add lines, if there is space
if not IsQueueFull then
begin
// set LyricLine to line to write to
if (LineCounter = 0) then
LyricLine := UpperLine
else if (LineCounter = 1) then
LyricLine := LowerLine
else
begin
// now the queue is full
LyricLine := QueueLine;
QueueFull := True;
end;
end
else
begin // rotate lines (round-robin-like)
LyricLine := UpperLine;
UpperLine := LowerLine;
LowerLine := QueueLine;
QueueLine := LyricLine;
end;
// reset line state
LyricLine.Reset();
// check if sentence has notes
if (Line <> nil) and (Length(Line.Note) > 0) then
begin
// copy values from SongLine to LyricLine
LyricLine.Start := Line.Start;
LyricLine.StartNote := Line.Note[0].Start;
LyricLine.Length := Line.Note[High(Line.Note)].Start +
Line.Note[High(Line.Note)].Length -
Line.Note[0].Start;
LyricLine.LastLine := Line.LastLine;
// copy words
SetLength(LyricLine.Words, Length(Line.Note));
for I := 0 to High(Line.Note) do
begin
LyricLine.Words[I].Start := Line.Note[I].Start;
LyricLine.Words[I].Length := Line.Note[I].Length;
LyricLine.Words[I].Text := Line.Note[I].Text;
LyricLine.Words[I].Freestyle := Line.Note[I].NoteType = ntFreestyle;
LyricLine.Text := LyricLine.Text + LyricLine.Words[I].Text;
end;
UpdateLineMetrics(LyricLine);
end;
// increase the counter
Inc(LCounter);
end;
{**
* Draws Lyrics.
* Draw just manages the Lyrics, drawing is done by a call of DrawLyrics.
* @param Beat: current Beat in Quarters
*}
procedure TLyricEngine.Draw(Beat: real);
begin
DrawLyrics(Beat);
LastDrawBeat := Beat;
end;
{**
* Main Drawing procedure.
*}
procedure TLyricEngine.DrawLyrics(Beat: real);
begin
DrawLyricsLine(UpperLineX, UpperLineW, UpperLineY, UpperLineH, UpperLine, Beat);
DrawLyricsLine(LowerLineX, LowerLineW, LowerLineY, LowerLineH, LowerLine, Beat);
end;
{**
* Draws a Player's icon.
*}
procedure TLyricEngine.DrawPlayerIcon(Player: byte; Enabled: boolean; X, Y: real; Size, Alpha: real);
var
IEnabled: byte;
begin
if Enabled then
IEnabled := 0
else
IEnabled := 1;
glEnable(GL_TEXTURE_2D);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glBindTexture(GL_TEXTURE_2D, PlayerIconTex[Player][IEnabled].TexNum);
glColor4f(1, 1, 1, Alpha);
glBegin(GL_QUADS);
glTexCoord2f(0, 0); glVertex2f(X, Y);
glTexCoord2f(0, 1); glVertex2f(X, Y + Size);
glTexCoord2f(1, 1); glVertex2f(X + Size, Y + Size);
glTexCoord2f(1, 0); glVertex2f(X + Size, Y);
glEnd;
glDisable(GL_BLEND);
glDisable(GL_TEXTURE_2D);
end;
{**
* Draws the Ball over the LyricLine if needed.
*}
procedure TLyricEngine.DrawBall(XBall, YBall, Alpha: real);
begin
glEnable(GL_TEXTURE_2D);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glBindTexture(GL_TEXTURE_2D, BallTex.TexNum);
glColor4f(1, 1, 1, Alpha);
glBegin(GL_QUADS);
glTexCoord2f(0, 0); glVertex2f(XBall - 10, YBall);
glTexCoord2f(0, 1); glVertex2f(XBall - 10, YBall + 20);
glTexCoord2f(1, 1); glVertex2f(XBall + 10, YBall + 20);
glTexCoord2f(1, 0); glVertex2f(XBall + 10, YBall);
glEnd;
glDisable(GL_BLEND);
glDisable(GL_TEXTURE_2D);
end;
procedure TLyricEngine.DrawLyricsWords(LyricLine: TLyricLine;
X, Y: real; StartWord, EndWord: integer);
var
I: integer;
PosX: real;
CurWord: PLyricWord;
begin
PosX := X;
// set word positions and line size and draw the line
for I := StartWord to EndWord do
begin
CurWord := @LyricLine.Words[I];
SetFontItalic(CurWord.Freestyle);
SetFontPos(PosX, Y);
glPrint(CurWord.Text);
PosX := PosX + CurWord.Width;
end;
end;
procedure TLyricEngine.UpdateLineMetrics(LyricLine: TLyricLine);
var
I: integer;
PosX: real;
CurWord: PLyricWord;
RequestWidth, RequestHeight: real;
begin
PosX := 0;
// setup font
SetFontStyle(FontStyle);
ResetFont();
// check if line is lower or upper line and set sizes accordingly
// Note: at the moment upper and lower lines have same width/height
// and this function is just called by AddLine() but this may change
// so that it is called by DrawLyricsLine().
//if (LyricLine = LowerLine) then
//begin
// RequestWidth := LowerLineW;
// RequestHeight := LowerLineH;
//end
//else
//begin
RequestWidth := UpperLineW;
RequestHeight := UpperLineH;
//end;
// set font size to a reasonable value
LyricLine.Height := RequestHeight * 0.9;
SetFontSize(LyricLine.Height);
LyricLine.Width := glTextWidth(LyricLine.Text);
// change font-size to fit into the lyric bar
if (LyricLine.Width > RequestWidth) then
begin
LyricLine.Height := Trunc(LyricLine.Height * (RequestWidth / LyricLine.Width));
// the line is very loooong, set font to at least 1px
if (LyricLine.Height < 1) then
LyricLine.Height := 1;
SetFontSize(LyricLine.Height);
LyricLine.Width := glTextWidth(LyricLine.Text);
end;
// calc word positions and widths
for I := 0 to High(LyricLine.Words) do
begin
CurWord := @LyricLine.Words[I];
// - if current word is italic but not the next word get the width of the
// italic font to avoid overlapping.
// - if two italic words follow each other use the normal style's
// width otherwise the spacing between the words will be too big.
// - if it is the line's last word use normal width
if CurWord.Freestyle and
(I+1 < Length(LyricLine.Words)) and
(not LyricLine.Words[I+1].Freestyle) then
begin
SetFontItalic(true);
end;
CurWord.X := PosX;
CurWord.Width := glTextWidth(CurWord.Text);
PosX := PosX + CurWord.Width;
SetFontItalic(false);
end;
end;
{**
* Draws one LyricLine
*}
procedure TLyricEngine.DrawLyricsLine(X, W, Y, H: real; Line: TLyricLine; Beat: real);
var
CurWord: PLyricWord; // current word
LastWord: PLyricWord; // last word in line
NextWord: PLyricWord; // word following current word
Progress: real; // progress of singing the current word
LyricX, LyricY: real; // left/top lyric position
WordY: real; // word y-position
LyricsEffect: TLyricsEffect;
Alpha: real; // alphalevel to fade out at end
ClipPlaneEq: array[0..3] of GLdouble; // clipping plane for slide effect
OutlineColor_act: TRGB; // outline color actual line
OutlineColor_dis: TRGB; // outline color next line
OutlineColor_en: TRGB; // outline color sing line
{// duet mode
IconSize: real; // size of player icons
IconAlpha: real; // alpha level of player icons
}
begin
// do not draw empty lines
if (Length(Line.Words) = 0) then
Exit;
{
// duet mode
IconSize := (2 * Height);
IconAlpha := Frac(Beat/(Resolution*4));
DrawPlayerIcon (0, True, X, Y + (42 - IconSize) / 2 , IconSize, IconAlpha);
DrawPlayerIcon (1, True, X + IconSize + 1, Y + (42 - IconSize) / 2, IconSize, IconAlpha);
DrawPlayerIcon (2, True, X + (IconSize + 1)*2, Y + (42 - IconSize) / 2, IconSize, IconAlpha);
}
// set font size and style
SetFontStyle(FontStyle);
ResetFont();
SetFontSize(Line.Height);
// center lyrics
LyricX := X + (W - Line.Width) / 2;
LyricY := Y + (H - Line.Height) / 2;
// get lyrics effect
LyricsEffect := TLyricsEffect(Ini.LyricsEffect);
// TODO: what about alpha in freetype outline fonts?
Alpha := 1;
// check if this line is active (at least its first note must be active)
if (Beat >= Line.StartNote) then
begin
// if this line just got active, CurWord is -1,
// this means we should try to make the first word active
if (Line.CurWord = -1) then
Line.CurWord := 0;
// check if the current active word is still active.
// Otherwise proceed to the next word if there is one in this line.
// Note: the max. value of Line.CurWord is High(Line.Words)
if (Line.CurWord < High(Line.Words)) and
(Beat >= Line.Words[Line.CurWord + 1].Start) then
begin
Inc(Line.CurWord);
end;
// determine current and last word in this line.
// If the end of the line is reached use the last word as current word.
LastWord := @Line.Words[High(Line.Words)];
CurWord := @Line.Words[Line.CurWord];
if (Line.CurWord+1 < Length(Line.Words)) then
NextWord := @Line.Words[Line.CurWord+1]
else
NextWord := nil;
// calc the progress of the lyrics effect
Progress := (Beat - CurWord.Start) / CurWord.Length;
if (Progress >= 1) then
Progress := 1;
if (Progress <= 0) then
Progress := 0;
// last word of this line finished, but this line did not hide -> fade out
if Line.LastLine and
(Beat > LastWord.Start + LastWord.Length) then
begin
Alpha := 1 - (Beat - (LastWord.Start + LastWord.Length)) / 15;
if (Alpha < 0) then
Alpha := 0;
end;
// outline color
SetOutlineColor(OutlineColor_act.R, OutlineColor_act.G, OutlineColor_act.B, Alpha);
// draw sentence before current word
if (LyricsEffect in [lfxSimple, lfxBall, lfxShift]) then
// only highlight current word and not that ones before in this line
glColorRGB(LineColor_en, Alpha)
else
glColorRGB(LineColor_act, Alpha);
DrawLyricsWords(Line, LyricX, LyricY, 0, Line.CurWord-1);
// draw rest of sentence (without current word)
glColorRGB(LineColor_en, Alpha);
if (NextWord <> nil) then
begin
// outline color
SetOutlineColor(OutlineColor_en.R, OutlineColor_en.G, OutlineColor_en.B, Alpha);
DrawLyricsWords(Line, LyricX + NextWord.X, LyricY,
Line.CurWord+1, High(Line.Words));
end;
// outline color
SetOutlineColor(OutlineColor_act.R, OutlineColor_act.G, OutlineColor_act.B, Alpha);
// draw current word
if (LyricsEffect in [lfxSimple, lfxBall, lfxShift]) then
begin
if (LyricsEffect = lfxShift) then
WordY := LyricY - 8 * (1-Progress)
else
WordY := LyricY;
// change the color of the current word
glColor4f(LineColor_act.r, LineColor_act.g, LineColor_act.b, Alpha);
DrawLyricsWords(Line, LyricX + CurWord.X, WordY, Line.CurWord, Line.CurWord);
end
// change color and zoom current word
else if (LyricsEffect = lfxZoom) then
begin
glPushMatrix;
// zoom at word center
glTranslatef(LyricX + CurWord.X + CurWord.Width/2,
LyricY + Line.Height/2, 0);
glScalef(1.0 + (1-Progress) * 0.5, 1.0 + (1-Progress) * 0.5, 1.0);
glColor4f(LineColor_act.r, LineColor_act.g, LineColor_act.b, Alpha);
DrawLyricsWords(Line, -CurWord.Width/2, -Line.Height/2, Line.CurWord, Line.CurWord);
glPopMatrix;
end
// split current word into active and non-active part
else if (LyricsEffect = lfxSlide) then
begin
// enable clipping and set clip equation coefficients to zeros
glEnable(GL_CLIP_PLANE0);
FillChar(ClipPlaneEq[0], SizeOf(ClipPlaneEq), 0);
glPushMatrix;
glTranslatef(LyricX + CurWord.X, LyricY, 0);
// clip non-active right part of the current word
ClipPlaneEq[0] := -1;
ClipPlaneEq[3] := CurWord.Width * Progress;
glClipPlane(GL_CLIP_PLANE0, @ClipPlaneEq);
// and draw active left part
glColor4f(LineColor_act.r, LineColor_act.g, LineColor_act.b, Alpha);
DrawLyricsWords(Line, 0, 0, Line.CurWord, Line.CurWord);
// clip active left part of the current word
ClipPlaneEq[0] := -ClipPlaneEq[0];
ClipPlaneEq[3] := -ClipPlaneEq[3];
glClipPlane(GL_CLIP_PLANE0, @ClipPlaneEq);
// and draw non-active right part
glColor4f(LineColor_en.r, LineColor_en.g, LineColor_en.b, Alpha);
DrawLyricsWords(Line, 0, 0, Line.CurWord, Line.CurWord);
glPopMatrix;
glDisable(GL_CLIP_PLANE0);
end;
// draw the ball onto the current word
if (LyricsEffect = lfxBall) then
begin
DrawBall(LyricX + CurWord.X + CurWord.Width * Progress,
LyricY - 15 - 15*sin(Progress * Pi), Alpha);
end;
end
else
begin
// this section is called if the whole line can be drawn at once and no
// word is highlighted.
// enable the upper, disable the lower line
if (Line = UpperLine) then
begin
// outline color
SetOutlineColor(OutlineColor_en.R, OutlineColor_en.G, OutlineColor_en.B, Alpha);
glColorRGB(LineColor_en)
end
else
begin
// outline color
SetOutlineColor(OutlineColor_dis.R, OutlineColor_dis.G, OutlineColor_dis.B, Alpha);
glColorRGB(LineColor_dis);
end;
DrawLyricsWords(Line, LyricX, LyricY, 0, High(Line.Words));
end;
// reset
SetOutlineColor(0,0,0,1);
end;
{**
* @returns a reference to the upper line
*}
function TLyricEngine.GetUpperLine(): TLyricLine;
begin
Result := UpperLine;
end;
{**
* @returns a reference to the lower line
*}
function TLyricEngine.GetLowerLine(): TLyricLine;
begin
Result := LowerLine;
end;
{**
* @returns the index of the upper line
*}
function TLyricEngine.GetUpperLineIndex(): integer;
const
QUEUE_SIZE = 3;
begin
// no line in queue
if (LineCounter <= 0) then
Result := -1
// no line has been removed from queue yet
else if (LineCounter <= QUEUE_SIZE) then
Result := 0
// lines have been removed from queue already
else
Result := LineCounter - QUEUE_SIZE;
end;
end.