{* 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 USong;
interface
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
{$I switches.inc}
uses
{$IFDEF MSWINDOWS}
Windows,
{$ELSE}
{$IFNDEF DARWIN}
syscall,
{$ENDIF}
baseunix,
UnixType,
{$ENDIF}
SysUtils,
Classes,
UPlatform,
ULog,
UTexture,
UCommon,
{$IFDEF DARWIN}
cthreads,
{$ENDIF}
{$IFDEF USE_PSEUDO_THREAD}
PseudoThread,
{$ENDIF}
UCatCovers,
UXMLSong,
UUnicodeUtils,
UTextEncoding,
UFilesystem,
UPath;
type
TSingMode = ( smNormal, smPartyMode, smPlaylistRandom );
TBPM = record
BPM: real;
StartBeat: real;
end;
TScore = record
Name: UTF8String;
Score: integer;
end;
TSong = class
private
FileLineNo : integer; // line, which is read last, for error reporting
function DecodeFilename(Filename: RawByteString): IPath;
function Solmizate(Note: integer; Type_: integer): string;
procedure ParseNote(LineNumber: integer; TypeP: char; StartP, DurationP, NoteP: integer; LyricS: UTF8String);
procedure NewSentence(LineNumberP: integer; Param1, Param2: integer);
function ParseLyricStringParam(const Line: RawByteString; var LinePos: integer): RawByteString;
function ParseLyricIntParam(const Line: RawByteString; var LinePos: integer): integer;
function ParseLyricFloatParam(const Line: RawByteString; var LinePos: integer): real;
function ParseLyricCharParam(const Line: RawByteString; var LinePos: integer): AnsiChar;
function ParseLyricText(const Line: RawByteString; var LinePos: integer): RawByteString;
function ReadTXTHeader(SongFile: TTextFileStream): boolean;
function ReadXMLHeader(const aFileName: IPath): boolean;
function GetFolderCategory(const aFileName: IPath): UTF8String;
function FindSongFile(Dir: IPath; Mask: UTF8String): IPath;
public
Path: IPath; // kust path component of file (only set if file was found)
Folder: UTF8String; // for sorting by folder (only set if file was found)
FileName: IPath; // just name component of file (only set if file was found)
// filenames
Cover: IPath;
Mp3: IPath;
Background: IPath;
Video: IPath;
// sorting methods
Genre: UTF8String;
Edition: UTF8String;
Language: UTF8String;
Title: UTF8String;
Artist: UTF8String;
Creator: UTF8String;
CoverTex: TTexture;
VideoGAP: real;
NotesGAP: integer;
Start: real; // in seconds
Finish: integer; // in miliseconds
Relative: boolean;
Resolution: integer;
BPM: array of TBPM;
GAP: real; // in miliseconds
Encoding: TEncoding;
Score: array[0..2] of array of TScore;
// these are used when sorting is enabled
Visible: boolean; // false if hidden, true if visible
Main: boolean; // false for songs, true for category buttons
OrderNum: integer; // has a number of category for category buttons and songs
OrderTyp: integer; // type of sorting for this button (0=name)
CatNumber: integer; // Count of Songs in Category for Cats and Number of Song in Category for Songs
Base : array[0..1] of integer;
Rel : array[0..1] of integer;
Mult : integer;
MultBPM : integer;
LastError: AnsiString;
function GetErrorLineNo: integer;
property ErrorLineNo: integer read GetErrorLineNo;
constructor Create(); overload;
constructor Create(const aFileName : IPath); overload;
function LoadSong: boolean;
function LoadXMLSong: boolean;
function Analyse(): boolean;
function AnalyseXML(): boolean;
procedure Clear();
end;
implementation
uses
StrUtils,
TextGL,
UIni,
UPathUtils,
UMusic, //needed for Lines
UNote; //needed for Player
const
// use USDX < 1.1 encoding for backward compatibility
DEFAULT_ENCODING = encCP1252;
constructor TSong.Create();
begin
inherited;
end;
// This may be changed, when we rewrite song select code.
// it is some kind of dirty, but imho the best possible
// solution as we do atm not support nested categorys.
// it works like the folder sorting in 1.0.1a
// folder is set to the first folder under the songdir
// so songs ~/.ultrastardx/songs/punk is in the same
// category as songs in shared/ultrastardx/songs are.
// note: folder is just the name of a category it has
// nothing to do with the path used for file loading
function TSong.GetFolderCategory(const aFileName: IPath): UTF8String;
var
I: Integer;
CurSongPath: IPath;
CurSongPathRel: IPath;
begin
Result := 'Unknown'; //default folder category, if we can't locate the song dir
for I := 0 to SongPaths.Count-1 do
begin
CurSongPath := SongPaths[I] as IPath;
if (aFileName.IsChildOf(CurSongPath, false)) then
begin
if (aFileName.IsChildOf(CurSongPath, true)) then
begin
// songs are in the "root" of the songdir => use songdir for the categorys name
Result := CurSongPath.ToUTF8; // TODO: remove trailing path-delim?
end
else
begin
// use the first subdirectory below CurSongPath as the category name
CurSongPathRel := aFileName.GetRelativePath(CurSongPath.AppendPathDelim);
Result := CurSongPathRel.SplitDirs[0].RemovePathDelim.ToUTF8;
end;
Exit;
end;
end;
end;
constructor TSong.Create(const aFileName: IPath);
begin
inherited Create();
Mult := 1;
MultBPM := 4;
LastError := '';
Self.Path := aFileName.GetPath;
Self.FileName := aFileName.GetName;
Self.Folder := GetFolderCategory(aFileName);
(*
if (aFileName.IsFile) then
begin
if ReadTXTHeader(aFileName) then
begin
LoadSong();
end
else
begin
Log.LogError('Error Loading SongHeader, abort Song Loading');
Exit;
end;
end;
*)
end;
function TSong.FindSongFile(Dir: IPath; Mask: UTF8String): IPath;
var
Iter: IFileIterator;
FileInfo: TFileInfo;
FileName: IPath;
begin
Iter := FileSystem.FileFind(Dir.Append(Mask), faDirectory);
if (Iter.HasNext) then
Result := Iter.Next.Name
else
Result := PATH_NONE;
end;
function TSong.DecodeFilename(Filename: RawByteString): IPath;
begin
Result := UPath.Path(DecodeStringUTF8(Filename, Encoding));
end;
type
EUSDXParseException = class(Exception);
{**
* Parses the Line string starting from LinePos for a parameter.
* Leading whitespace is trimmed, same applies to the first trailing whitespace.
* After the call LinePos will point to the position after the first trailing
* whitespace.
*
* Raises an EUSDXParseException if no string was found.
*
* Example:
* ParseLyricParam(Line:'Param0 Param1 Param2', LinePos:8, ...)
* -> Param:'Param1', LinePos:16 (= start of 'Param2')
*}
function TSong.ParseLyricStringParam(const Line: RawByteString; var LinePos: integer): RawByteString;
var
Start: integer;
OldLinePos: integer;
const
Whitespace = [#9, ' '];
begin
OldLinePos := LinePos;
Start := 0;
while (LinePos <= Length(Line)) do
begin
if (Line[LinePos] in Whitespace) then
begin
// check for end of param
if (Start > 0) then
Break;
end
// check for beginning of param
else if (Start = 0) then
begin
Start := LinePos;
end;
Inc(LinePos);
end;
// check if param was found
if (Start = 0) then
begin
LinePos := OldLinePos;
raise EUSDXParseException.Create('String expected');
end
else
begin
// copy param without trailing whitespace
Result := Copy(Line, Start, LinePos-Start);
// skip first trailing whitespace (if not at EOL)
if (LinePos <= Length(Line)) then
Inc(LinePos);
end;
end;
function TSong.ParseLyricIntParam(const Line: RawByteString; var LinePos: integer): integer;
var
Str: RawByteString;
OldLinePos: integer;
begin
OldLinePos := LinePos;
Str := ParseLyricStringParam(Line, LinePos);
try
Result := StrToInt(Str);
except // on EConvertError
LinePos := OldLinePos;
raise EUSDXParseException.Create('Integer expected');
end;
end;
function TSong.ParseLyricFloatParam(const Line: RawByteString; var LinePos: integer): real;
var
Str: RawByteString;
OldLinePos: integer;
begin
OldLinePos := LinePos;
Str := ParseLyricStringParam(Line, LinePos);
try
Result := StrToFloat(Str);
except // on EConvertError
LinePos := OldLinePos;
raise EUSDXParseException.Create('Float expected');
end;
end;
function TSong.ParseLyricCharParam(const Line: RawByteString; var LinePos: integer): AnsiChar;
var
Str: RawByteString;
OldLinePos: integer;
begin
OldLinePos := LinePos;
Str := ParseLyricStringParam(Line, LinePos);
if (Length(Str) <> 1) then
begin
LinePos := OldLinePos;
raise EUSDXParseException.Create('Character expected');
end;
Result := Str[1];
end;
{**
* Returns the rest of the line from LinePos as lyric text.
* Leading and trailing whitespace is not trimmed.
*}
function TSong.ParseLyricText(const Line: RawByteString; var LinePos: integer): RawByteString;
begin
if (LinePos > Length(Line)) then
Result := ''
else
begin
Result := Copy(Line, LinePos, Length(Line)-LinePos+1);
LinePos := Length(Line)+1;
end;
end;
//Load TXT Song
function TSong.LoadSong(): boolean;
var
CurLine: RawByteString;
LinePos: integer;
Count: integer;
Both: boolean;
Param0: AnsiChar;
Param1: integer;
Param2: integer;
Param3: integer;
ParamLyric: UTF8String;
I: integer;
NotesFound: boolean;
SongFile: TTextFileStream;
FileNamePath: IPath;
begin
Result := false;
LastError := '';
FileNamePath := Path.Append(FileName);
if not FileNamePath.IsFile() then
begin
LastError := 'ERROR_CORRUPT_SONG_FILE_NOT_FOUND';
Log.LogError('File not found: "' + FileNamePath.ToNative + '"', 'TSong.LoadSong()');
Exit;
end;
MultBPM := 4; // multiply beat-count of note by 4
Mult := 1; // accuracy of measurement of note
Rel[0] := 0;
Both := false;
if Length(Player) = 2 then
Both := true;
try
// Open song file for reading.....
SongFile := TMemTextFileStream.Create(FileNamePath, fmOpenRead);
try
//Search for Note Beginning
FileLineNo := 0;
NotesFound := false;
while (SongFile.ReadLine(CurLine)) do
begin
Inc(FileLineNo);
if (Length(CurLine) > 0) and (CurLine[1] in [':', 'F', '*']) then
begin
NotesFound := true;
Break;
end;
end;
if (not NotesFound) then
begin //Song File Corrupted - No Notes
Log.LogError('Could not load txt File, no notes found: ' + FileNamePath.ToNative);
LastError := 'ERROR_CORRUPT_SONG_NO_NOTES';
Exit;
end;
SetLength(Lines, 2);
for Count := 0 to High(Lines) do
begin
Lines[Count].High := 0;
Lines[Count].Number := 1;
Lines[Count].Current := 0;
Lines[Count].Resolution := self.Resolution;
Lines[Count].NotesGAP := self.NotesGAP;
Lines[Count].ScoreValue := 0;
//Add first line and set some standard values to fields
//see procedure NewSentence for further explantation
//concerning most of these values
SetLength(Lines[Count].Line, 1);
Lines[Count].Line[0].HighNote := -1;
Lines[Count].Line[0].LastLine := false;
Lines[Count].Line[0].BaseNote := High(Integer);
Lines[Count].Line[0].TotalNotes := 0;
end;
while true do
begin
LinePos := 0;
Param0 := ParseLyricCharParam(CurLine, LinePos);
if (Param0 = 'E') then
begin
Break
end
else if (Param0 in [':', '*', 'F']) then
begin
// read notes
Param1 := ParseLyricIntParam(CurLine, LinePos);
Param2 := ParseLyricIntParam(CurLine, LinePos);
Param3 := ParseLyricIntParam(CurLine, LinePos);
ParamLyric := ParseLyricText(CurLine, LinePos);
//Check for ZeroNote
if Param2 = 0 then
Log.LogError('Found zero-length note at "'+Param0+' '+IntToStr(Param1)+' '+IntToStr(Param2)+' '+IntToStr(Param3)+ParamLyric+'" -> Note ignored!')
else
begin
// add notes
if not Both then
// P1
ParseNote(0, Param0, (Param1+Rel[0]) * Mult, Param2 * Mult, Param3, ParamLyric)
else
begin
// P1 + P2
ParseNote(0, Param0, (Param1+Rel[0]) * Mult, Param2 * Mult, Param3, ParamLyric);
ParseNote(1, Param0, (Param1+Rel[1]) * Mult, Param2 * Mult, Param3, ParamLyric);
end;
end; //Zeronote check
end // if
else if Param0 = '-' then
begin
// reads sentence
Param1 := ParseLyricIntParam(CurLine, LinePos);
if self.Relative then
Param2 := ParseLyricIntParam(CurLine, LinePos); // read one more data for relative system
// new sentence
if not Both then
// P1
NewSentence(0, (Param1 + Rel[0]) * Mult, Param2)
else
begin
// P1 + P2
NewSentence(0, (Param1 + Rel[0]) * Mult, Param2);
NewSentence(1, (Param1 + Rel[1]) * Mult, Param2);
end;
end // if
else if Param0 = 'B' then
begin
SetLength(self.BPM, Length(self.BPM) + 1);
self.BPM[High(self.BPM)].StartBeat := ParseLyricFloatParam(CurLine, LinePos);
self.BPM[High(self.BPM)].StartBeat := self.BPM[High(self.BPM)].StartBeat + Rel[0];
self.BPM[High(self.BPM)].BPM := ParseLyricFloatParam(CurLine, LinePos);
self.BPM[High(self.BPM)].BPM := self.BPM[High(self.BPM)].BPM * Mult * MultBPM;
end;
// Read next line in File
if (not SongFile.ReadLine(CurLine)) then
Break;
Inc(FileLineNo);
end; // while
finally
SongFile.Free;
end;
except
on E: Exception do
begin
Log.LogError(Format('Error loading file: "%s" in line %d,%d: %s',
[FileNamePath.ToNative, FileLineNo, LinePos, E.Message]));
Exit;
end;
end;
for I := 0 to High(Lines) do
begin
if ((Both) or (I = 0)) then
begin
if (Length(Lines[I].Line) < 2) then
begin
LastError := 'ERROR_CORRUPT_SONG_NO_BREAKS';
Log.LogError('Error loading file: Can''t find any linebreaks in "' + FileNamePath.ToNative + '"');
exit;
end;
if (Lines[I].Line[Lines[I].High].HighNote < 0) then
begin
SetLength(Lines[I].Line, Lines[I].Number - 1);
Lines[I].High := Lines[I].High - 1;
Lines[I].Number := Lines[I].Number - 1;
Log.LogError('Error loading Song, sentence w/o note found in last line before E: ' + FileNamePath.ToNative);
end;
end;
end;
for Count := 0 to High(Lines) do
begin
if (High(Lines[Count].Line) >= 0) then
Lines[Count].Line[High(Lines[Count].Line)].LastLine := true;
end;
Result := true;
end;
//Load XML Song
function TSong.LoadXMLSong(): boolean;
var
Count: integer;
Both: boolean;
Param1: integer;
Param2: integer;
Param3: integer;
ParamS: string;
I, J: integer;
NoteIndex: integer;
NoteType: char;
SentenceEnd, Rest, Time: integer;
Parser: TParser;
FileNamePath: IPath;
begin
Result := false;
LastError := '';
FileNamePath := Path.Append(FileName);
if not FileNamePath.IsFile() then
begin
Log.LogError('File not found: "' + FileNamePath.ToNative + '"', 'TSong.LoadSong()');
exit;
end;
MultBPM := 4; // multiply beat-count of note by 4
Mult := 1; // accuracy of measurement of note
Lines[0].ScoreValue := 0;
self.Relative := false;
Rel[0] := 0;
Both := false;
if Length(Player) = 2 then
Both := true;
Parser := TParser.Create;
Parser.Settings.DashReplacement := '~';
for Count := 0 to High(Lines) do
begin
Lines[Count].High := 0;
Lines[Count].Number := 1;
Lines[Count].Current := 0;
Lines[Count].Resolution := self.Resolution;
Lines[Count].NotesGAP := self.NotesGAP;
Lines[Count].ScoreValue := 0;
//Add first line and set some standard values to fields
//see procedure NewSentence for further explantation
//concerning most of these values
SetLength(Lines[Count].Line, 1);
Lines[Count].Line[0].HighNote := -1;
Lines[Count].Line[0].LastLine := false;
Lines[Count].Line[0].BaseNote := High(Integer);
Lines[Count].Line[0].TotalNotes := 0;
end;
//Try to Parse the Song
if Parser.ParseSong(FileNamePath) then
begin
//Writeln('XML Inputfile Parsed succesful');
//Start write parsed information to Song
//Notes Part
for I := 0 to High(Parser.SongInfo.Sentences) do
begin
//Add Notes
for J := 0 to High(Parser.SongInfo.Sentences[I].Notes) do
begin
case Parser.SongInfo.Sentences[I].Notes[J].NoteTyp of
NT_Normal: NoteType := ':';
NT_Golden: NoteType := '*';
NT_Freestyle: NoteType := 'F';
end;
Param1:=Parser.SongInfo.Sentences[I].Notes[J].Start; //Note Start
Param2:=Parser.SongInfo.Sentences[I].Notes[J].Duration; //Note Duration
Param3:=Parser.SongInfo.Sentences[I].Notes[J].Tone; //Note Tone
ParamS:=' ' + Parser.SongInfo.Sentences[I].Notes[J].Lyric; //Note Lyric
if not Both then
// P1
ParseNote(0, NoteType, (Param1+Rel[0]) * Mult, Param2 * Mult, Param3, ParamS)
else
begin
// P1 + P2
ParseNote(0, NoteType, (Param1+Rel[0]) * Mult, Param2 * Mult, Param3, ParamS);
ParseNote(1, NoteType, (Param1+Rel[1]) * Mult, Param2 * Mult, Param3, ParamS);
end;
end; //J Forloop
//Add Sentence break
if (I < High(Parser.SongInfo.Sentences)) then
begin
SentenceEnd := Parser.SongInfo.Sentences[I].Notes[High(Parser.SongInfo.Sentences[I].Notes)].Start + Parser.SongInfo.Sentences[I].Notes[High(Parser.SongInfo.Sentences[I].Notes)].Duration;
Rest := Parser.SongInfo.Sentences[I+1].Notes[0].Start - SentenceEnd;
//Calculate Time
case Rest of
0, 1: Time := Parser.SongInfo.Sentences[I+1].Notes[0].Start;
2: Time := Parser.SongInfo.Sentences[I+1].Notes[0].Start - 1;
3: Time := Parser.SongInfo.Sentences[I+1].Notes[0].Start - 2;
else
if (Rest >= 4) then
Time := SentenceEnd + 2
else //Sentence overlapping :/
Time := Parser.SongInfo.Sentences[I+1].Notes[0].Start;
end;
// new sentence
if not Both then // P1
NewSentence(0, (Time + Rel[0]) * Mult, Param2)
else
begin // P1 + P2
NewSentence(0, (Time + Rel[0]) * Mult, Param2);
NewSentence(1, (Time + Rel[1]) * Mult, Param2);
end;
end;
end;
//End write parsed information to Song
Parser.Free;
end
else
begin
Log.LogError('Could not parse inputfile: ' + FileNamePath.ToNative);
exit;
end;
for Count := 0 to High(Lines) do
begin
Lines[Count].Line[High(Lines[Count].Line)].LastLine := true;
end;
Result := true;
end;
function TSong.ReadXMLHeader(const aFileName : IPath): boolean;
var
Done : byte;
Parser : TParser;
FileNamePath: IPath;
begin
Result := true;
Done := 0;
//Parse XML
Parser := TParser.Create;
Parser.Settings.DashReplacement := '~';
FileNamePath := Self.Path.Append(Self.FileName);
if Parser.ParseSong(FileNamePath) then
begin
//-----------
//Required Attributes
//-----------
//Title
self.Title := Parser.SongInfo.Header.Title;
//Add Title Flag to Done
Done := Done or 1;
//Artist
self.Artist := Parser.SongInfo.Header.Artist;
//Add Artist Flag to Done
Done := Done or 2;
//MP3 File //Test if Exists
Self.Mp3 := FindSongFile(Self.Path, '*.mp3');
//Add Mp3 Flag to Done
if (Self.Path.Append(Self.Mp3).IsFile()) then
Done := Done or 4;
//Beats per Minute
SetLength(self.BPM, 1);
self.BPM[0].StartBeat := 0;
self.BPM[0].BPM := (Parser.SongInfo.Header.BPM * Parser.SongInfo.Header.Resolution/4 ) * Mult * MultBPM;
//Add BPM Flag to Done
if self.BPM[0].BPM <> 0 then
Done := Done or 8;
//---------
//Additional Header Information
//---------
// Gap
self.GAP := Parser.SongInfo.Header.Gap;
//Cover Picture
self.Cover := FindSongFile(Path, '*[CO].jpg');
//Background Picture
self.Background := FindSongFile(Path, '*[BG].jpg');
// Video File
// self.Video := Value
// Video Gap
// self.VideoGAP := StrtoFloatI18n( Value )
//Genre Sorting
self.Genre := Parser.SongInfo.Header.Genre;
//Edition Sorting
self.Edition := Parser.SongInfo.Header.Edition;
//Year Sorting
//Parser.SongInfo.Header.Year
//Language Sorting
self.Language := Parser.SongInfo.Header.Language;
end
else
Log.LogError('File incomplete or not SingStar XML (A): ' + aFileName.ToNative);
Parser.Free;
//Check if all Required Values are given
if (Done <> 15) then
begin
Result := false;
if (Done and 8) = 0 then //No BPM Flag
Log.LogError('BPM tag missing: ' + self.FileName.ToNative)
else if (Done and 4) = 0 then //No MP3 Flag
Log.LogError('MP3 tag/file missing: ' + self.FileName.ToNative)
else if (Done and 2) = 0 then //No Artist Flag
Log.LogError('Artist tag missing: ' + self.FileName.ToNative)
else if (Done and 1) = 0 then //No Title Flag
Log.LogError('Title tag missing: ' + self.FileName.ToNative)
else //unknown Error
Log.LogError('File incomplete or not SingStar XML (B - '+ inttostr(Done) +'): ' + aFileName.ToNative);
end;
end;
{**
* "International" StrToFloat variant. Uses either ',' or '.' as decimal
* separator.
*}
function StrToFloatI18n(const Value: string): extended;
var
TempValue : string;
begin
TempValue := Value;
if (Pos(',', TempValue) <> 0) then
TempValue[Pos(',', TempValue)] := '.';
Result := StrToFloatDef(TempValue, 0);
end;
function TSong.ReadTXTHeader(SongFile: TTextFileStream): boolean;
var
Line, Identifier: string;
Value: string;
SepPos: integer; // separator position
Done: byte; // bit-vector of mandatory fields
EncFile: IPath; // encoded filename
FullFileName: string;
begin
Result := true;
Done := 0;
FullFileName := Path.Append(Filename).ToNative;
//Read first Line
SongFile.ReadLine(Line);
if (Length(Line) <= 0) then
begin
Log.LogError('File starts with empty line: ' + FullFileName,
'TSong.ReadTXTHeader');
Result := false;
Exit;
end;
// check if file begins with a UTF-8 BOM, if so set encoding to UTF-8
if (CheckReplaceUTF8BOM(Line)) then
Encoding := encUTF8;
//Read Lines while Line starts with # or its empty
while (Length(Line) = 0) or (Line[1] = '#') do
begin
//Increase Line Number
Inc (FileLineNo);
SepPos := Pos(':', Line);
//Line has no Seperator, ignore non header field
if (SepPos = 0) then
Continue;
//Read Identifier and Value
Identifier := UpperCase(Trim(Copy(Line, 2, SepPos - 2))); //Uppercase is for Case Insensitive Checks
Value := Trim(Copy(Line, SepPos + 1, Length(Line) - SepPos));
//Check the Identifier (If Value is given)
if (Length(Value) = 0) then
begin
Log.LogWarn('Empty field "'+Identifier+'" in file ' + FullFileName,
'TSong.ReadTXTHeader');
end
else
begin
//-----------
//Required Attributes
//-----------
if (Identifier = 'TITLE') then
begin
DecodeStringUTF8(Value, Title, Encoding);
//Add Title Flag to Done
Done := Done or 1;
end
else if (Identifier = 'ARTIST') then
begin
DecodeStringUTF8(Value, Artist, Encoding);
//Add Artist Flag to Done
Done := Done or 2;
end
//MP3 File
else if (Identifier = 'MP3') then
begin
EncFile := DecodeFilename(Value);
if (Self.Path.Append(EncFile).IsFile) then
begin
self.Mp3 := EncFile;
//Add Mp3 Flag to Done
Done := Done or 4;
end;
end
//Beats per Minute
else if (Identifier = 'BPM') then
begin
SetLength(self.BPM, 1);
self.BPM[0].StartBeat := 0;
self.BPM[0].BPM := StrToFloatI18n( Value ) * Mult * MultBPM;
if self.BPM[0].BPM <> 0 then
begin
//Add BPM Flag to Done
Done := Done or 8;
end;
end
//---------
//Additional Header Information
//---------
// Gap
else if (Identifier = 'GAP') then
begin
self.GAP := StrToFloatI18n(Value);
end
//Cover Picture
else if (Identifier = 'COVER') then
begin
self.Cover := DecodeFilename(Value);
end
//Background Picture
else if (Identifier = 'BACKGROUND') then
begin
self.Background := DecodeFilename(Value);
end
// Video File
else if (Identifier = 'VIDEO') then
begin
EncFile := DecodeFilename(Value);
if (self.Path.Append(EncFile).IsFile) then
self.Video := EncFile
else
Log.LogError('Can''t find video file in song: ' + FullFileName);
end
// Video Gap
else if (Identifier = 'VIDEOGAP') then
begin
self.VideoGAP := StrToFloatI18n( Value )
end
//Genre Sorting
else if (Identifier = 'GENRE') then
begin
DecodeStringUTF8(Value, Genre, Encoding)
end
//Edition Sorting
else if (Identifier = 'EDITION') then
begin
DecodeStringUTF8(Value, Edition, Encoding)
end
//Creator Tag
else if (Identifier = 'CREATOR') then
begin
DecodeStringUTF8(Value, Creator, Encoding)
end
//Language Sorting
else if (Identifier = 'LANGUAGE') then
begin
DecodeStringUTF8(Value, Language, Encoding)
end
// Song Start
else if (Identifier = 'START') then
begin
self.Start := StrToFloatI18n( Value )
end
// Song Ending
else if (Identifier = 'END') then
begin
TryStrtoInt(Value, self.Finish)
end
// Resolution
else if (Identifier = 'RESOLUTION') then
begin
TryStrtoInt(Value, self.Resolution)
end
// Notes Gap
else if (Identifier = 'NOTESGAP') then
begin
TryStrtoInt(Value, self.NotesGAP)
end
// Relative Notes
else if (Identifier = 'RELATIVE') then
begin
if (UpperCase(Value) = 'YES') then
self.Relative := true;
end
// File encoding
else if (Identifier = 'ENCODING') then
begin
self.Encoding := ParseEncoding(Value, DEFAULT_ENCODING);
end;
end; // End check for non-empty Value
// read next line
if (not SongFile.ReadLine(Line)) then
begin
Result := false;
Log.LogError('File incomplete or not Ultrastar txt (A): ' + FullFileName);
Break;
end;
end; // while
if self.Cover.IsUnset then
self.Cover := FindSongFile(Path, '*[CO].jpg');
//Check if all Required Values are given
if (Done <> 15) then
begin
Result := false;
if (Done and 8) = 0 then //No BPM Flag
Log.LogError('BPM tag missing: ' + FullFileName)
else if (Done and 4) = 0 then //No MP3 Flag
Log.LogError('MP3 tag/file missing: ' + FullFileName)
else if (Done and 2) = 0 then //No Artist Flag
Log.LogError('Artist tag missing: ' + FullFileName)
else if (Done and 1) = 0 then //No Title Flag
Log.LogError('Title tag missing: ' + FullFileName)
else //unknown Error
Log.LogError('File incomplete or not Ultrastar txt (B - '+ inttostr(Done) +'): ' + FullFileName);
end;
end;
function TSong.GetErrorLineNo: integer;
begin
if (LastError='ERROR_CORRUPT_SONG_ERROR_IN_LINE') then
Result := FileLineNo
else
Result := -1;
end;
function TSong.Solmizate(Note: integer; Type_: integer): string;
begin
case (Type_) of
1: // european
begin
case (Note mod 12) of
0..1: Result := ' do ';
2..3: Result := ' re ';
4: Result := ' mi ';
5..6: Result := ' fa ';
7..8: Result := ' sol ';
9..10: Result := ' la ';
11: Result := ' si ';
end;
end;
2: // japanese
begin
case (Note mod 12) of
0..1: Result := ' do ';
2..3: Result := ' re ';
4: Result := ' mi ';
5..6: Result := ' fa ';
7..8: Result := ' so ';
9..10: Result := ' la ';
11: Result := ' shi ';
end;
end;
3: // american
begin
case (Note mod 12) of
0..1: Result := ' do ';
2..3: Result := ' re ';
4: Result := ' mi ';
5..6: Result := ' fa ';
7..8: Result := ' sol ';
9..10: Result := ' la ';
11: Result := ' ti ';
end;
end;
end; // case
end;
procedure TSong.ParseNote(LineNumber: integer; TypeP: char; StartP, DurationP, NoteP: integer; LyricS: UTF8String);
begin
if (Ini.Solmization <> 0) then
LyricS := Solmizate(NoteP, Ini.Solmization);
with Lines[LineNumber].Line[Lines[LineNumber].High] do
begin
SetLength(Note, Length(Note) + 1);
HighNote := High(Note);
Note[HighNote].Start := StartP;
if HighNote = 0 then
begin
if Lines[LineNumber].Number = 1 then
Start := -100;
//Start := Note[HighNote].Start;
end;
Note[HighNote].Length := DurationP;
// back to the normal system with normal, golden and now freestyle notes
case TypeP of
'F': Note[HighNote].NoteType := ntFreestyle;
':': Note[HighNote].NoteType := ntNormal;
'*': Note[HighNote].NoteType := ntGolden;
end;
//add this notes value ("notes length" * "notes scorefactor") to the current songs entire value
Inc(Lines[LineNumber].ScoreValue, Note[HighNote].Length * ScoreFactor[Note[HighNote].NoteType]);
//and to the current lines entire value
Inc(TotalNotes, Note[HighNote].Length * ScoreFactor[Note[HighNote].NoteType]);
Note[HighNote].Tone := NoteP;
//if a note w/ a deeper pitch then the current basenote is found
//we replace the basenote w/ the current notes pitch
if Note[HighNote].Tone < BaseNote then
BaseNote := Note[HighNote].Tone;
DecodeStringUTF8(LyricS, Note[HighNote].Text, Encoding);
Lyric := Lyric + Note[HighNote].Text;
End_ := Note[HighNote].Start + Note[HighNote].Length;
end; // with
end;
procedure TSong.NewSentence(LineNumberP: integer; Param1, Param2: integer);
var
I: integer;
begin
if (Lines[LineNumberP].Line[Lines[LineNumberP].High].HighNote <> -1) then
begin //create a new line
SetLength(Lines[LineNumberP].Line, Lines[LineNumberP].Number + 1);
Inc(Lines[LineNumberP].High);
Inc(Lines[LineNumberP].Number);
end
else
begin //use old line if it there were no notes added since last call of NewSentence
Log.LogError('Error loading Song, sentence w/o note found in line ' +
InttoStr(FileLineNo) + ': ' + Filename.ToNative);
end;
Lines[LineNumberP].Line[Lines[LineNumberP].High].HighNote := -1;
//set the current lines value to zero
//it will be incremented w/ the value of every added note
Lines[LineNumberP].Line[Lines[LineNumberP].High].TotalNotes := 0;
//basenote is the pitch of the deepest note, it is used for note drawing.
//if a note with a less value than the current sentences basenote is found,
//basenote will be set to this notes pitch. Therefore the initial value of
//this field has to be very high.
Lines[LineNumberP].Line[Lines[LineNumberP].High].BaseNote := High(Integer);
if self.Relative then
begin
Lines[LineNumberP].Line[Lines[LineNumberP].High].Start := Param1;
Rel[LineNumberP] := Rel[LineNumberP] + Param2;
end
else
Lines[LineNumberP].Line[Lines[LineNumberP].High].Start := Param1;
Lines[LineNumberP].Line[Lines[LineNumberP].High].LastLine := false;
end;
procedure TSong.Clear();
begin
//Main Information
Title := '';
Artist := '';
//Sortings:
Genre := 'Unknown';
Edition := 'Unknown';
Language := 'Unknown'; //Language Patch
// set to default encoding
Encoding := DEFAULT_ENCODING;
//Required Information
Mp3 := PATH_NONE;
SetLength(BPM, 0);
GAP := 0;
Start := 0;
Finish := 0;
//Additional Information
Background := PATH_NONE;
Cover := PATH_NONE;
Video := PATH_NONE;
VideoGAP := 0;
NotesGAP := 0;
Resolution := 4;
Creator := '';
Relative := false;
end;
function TSong.Analyse(): boolean;
var
SongFile: TTextFileStream;
begin
Result := false;
//Reset LineNo
FileLineNo := 0;
//Open File and set File Pointer to the beginning
SongFile := TMemTextFileStream.Create(Self.Path.Append(Self.FileName), fmOpenRead);
try
//Clear old Song Header
Self.clear;
//Read Header
Result := Self.ReadTxTHeader(SongFile)
finally
SongFile.Free;
end;
end;
function TSong.AnalyseXML(): boolean;
begin
Result := false;
//Reset LineNo
FileLineNo := 0;
//Clear old Song Header
self.clear;
//Read Header
Result := self.ReadXMLHeader( FileName );
end;
end.