{* 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 UXMLSong; interface {$IFDEF FPC} {$MODE Delphi} {$ENDIF} {$I switches.inc} uses Classes, UPath, UUnicodeUtils; type TNote = record Start: Cardinal; Duration: Cardinal; Tone: Integer; NoteTyp: Byte; Lyric: UTF8String; end; ANote = array of TNote; TSentence = record Singer: Byte; Duration: Cardinal; Notes: ANote; end; ASentence = array of TSentence; TSongInfo = record ID: Cardinal; DualChannel: Boolean; Header: record Artist: UTF8String; Title: UTF8String; Gap: Cardinal; BPM: Real; Resolution: Byte; Edition: UTF8String; Genre: UTF8String; Year: UTF8String; Language: UTF8String; end; CountSentences: Cardinal; Sentences: ASentence; end; TParser = class private SSFile: TStringList; ParserState: Byte; CurPosinSong: Cardinal; //Cur Beat Pos in the Song CurDuettSinger: Byte; //Who sings this Part? BindLyrics: Boolean; //Should the Lyrics be bind to the last Word (no Space) FirstNote: Boolean; //Is this the First Note found? For Gap calculating function ParseLine(Line: RawByteString): Boolean; public SongInfo: TSongInfo; ErrorMessage: string; Edition: UTF8String; SingstarVersion: string; Settings: record DashReplacement: Char; end; constructor Create; function ParseConfigForEdition(const Filename: IPath): String; function ParseSongHeader(const Filename: IPath): Boolean; //Parse Song Header only function ParseSong (const Filename: IPath): Boolean; //Parse whole Song end; const PS_None = 0; PS_Melody = 1; PS_Sentence = 2; NT_Normal = 1; NT_Freestyle = 0; NT_Golden = 2; DS_Player1 = 1; DS_Player2 = 2; DS_Both = 3; implementation uses SysUtils, StrUtils; constructor TParser.Create; begin inherited Create; ErrorMessage := ''; DecimalSeparator := '.'; end; function TParser.ParseSong(const Filename: IPath): Boolean; var I: Integer; FileStream: TBinaryFileStream; begin Result := False; if Filename.IsFile() then begin ErrorMessage := 'Can''t open melody.xml file'; SSFile := TStringList.Create; FileStream := TBinaryFileStream.Create(Filename, fmOpenRead); try SSFile.LoadFromStream(FileStream); ErrorMessage := ''; Result := True; I := 0; SongInfo.CountSentences := 0; CurDuettSinger := DS_Both; //Both is Singstar Standard CurPosinSong := 0; //Start at Pos 0 BindLyrics := True; //Dont start with Space FirstNote := True; //First Note found should be the First Note ;) SongInfo.Header.Language := ''; SongInfo.Header.Edition := Edition; SongInfo.DualChannel := False; ParserState := PS_None; SetLength(SongInfo.Sentences, 0); while Result and (I < SSFile.Count) do begin Result := ParseLine(SSFile.Strings[I]); Inc(I); end; finally SSFile.Free; FileStream.Free; end; end; end; function TParser.ParseSongHeader (const Filename: IPath): Boolean; var I: Integer; Stream: TBinaryFileStream; begin Result := False; if Filename.IsFile() then begin SSFile := TStringList.Create; Stream := TBinaryFileStream.Create(Filename, fmOpenRead); try SSFile.LoadFromStream(Stream); If (SSFile.Count > 0) then begin Result := True; I := 0; SongInfo.CountSentences := 0; CurDuettSinger := DS_Both; //Both is Singstar Standard CurPosinSong := 0; //Start at Pos 0 BindLyrics := True; //Dont start with Space FirstNote := True; //First Note found should be the First Note ;) SongInfo.ID := 0; SongInfo.Header.Language := ''; SongInfo.Header.Edition := Edition; SongInfo.DualChannel := False; ParserState := PS_None; While (SongInfo.ID < 4) AND Result And (I < SSFile.Count) do begin Result := ParseLine(SSFile.Strings[I]); Inc(I); end; end else ErrorMessage := 'Can''t open melody.xml file'; finally SSFile.Free; Stream.Free; end; end else ErrorMessage := 'Can''t find melody.xml file'; end; Function TParser.ParseLine(Line: String): Boolean; var Tag: String; Values: String; AValues: Array of Record Name: String; Value: String; end; I, J, K: Integer; Duration, Tone: Integer; Lyric: String; NoteType: Byte; Procedure MakeValuesArray; var Len, Pos, State, StateChange: Integer; begin Len := -1; SetLength(AValues, Len + 1); Pos := 1; State := 0; While (Pos <= Length(Values)) AND (Pos <> 0) do begin Case State of 0: begin //Search for ValueName If (Values[Pos] <> ' ') AND (Values[Pos] <> '=') then begin //Found Something State := 1; //State search for '=' StateChange := Pos; //Save Pos of Change Pos := PosEx('=', Values, Pos + 1); end else Inc(Pos); //When nothing found then go to next char end; 1: begin //Search for Equal Mark //Add New Value Inc(Len); SetLength(AValues, Len + 1); AValues[Len].Name := UpperCase(Copy(Values, StateChange, Pos - StateChange)); State := 2; //Now Search for starting '"' StateChange := Pos; //Save Pos of Change Pos := PosEx('"', Values, Pos + 1); end; 2: begin //Search for starting '"' or ' ' <- End if there was no " If (Values[Pos] = '"') then begin //Found starting '"' State := 3; //Now Search for ending '"' StateChange := Pos; //Save Pos of Change Pos := PosEx('"', Values, Pos + 1); end else If (Values[Pos] = ' ') then //Found ending Space begin //Save Value to Array AValues[Len].Value := Copy(Values, StateChange + 1, Pos - StateChange - 1); //Search for next Valuename State := 0; StateChange := Pos; Inc(Pos); end; end; 3: begin //Search for ending '"' //Save Value to Array AValues[Len].Value := Copy(Values, StateChange + 1, Pos - StateChange - 1); //Search for next Valuename State := 0; StateChange := Pos; Inc(Pos); end; end; If (State >= 2) then begin //Save Last Value AValues[Len].Value := Copy(Values, StateChange + 1, Length(Values) - StateChange); end; end; end; begin Result := True; Line := Trim(Line); If (Length(Line) > 0) then begin I := Pos('<', Line); J := PosEx(' ', Line, I+1); K := PosEx('>', Line, I+1); If (J = 0) then J := K Else If (K < J) AND (K <> 0) then J := K; //Use nearest Tagname End indicator Tag := UpperCase(copy(Line, I + 1, J - I - 1)); Values := copy(Line, J + 1, K - J - 1); Case ParserState of PS_None: begin//Search for Melody Tag If (Tag = 'MELODY') then begin Inc(SongInfo.ID); //Inc SongID when header Information is added MakeValuesArray; For I := 0 to High(AValues) do begin If (AValues[I].Name = 'TEMPO') then begin SongInfo.Header.BPM := StrtoFloatDef(AValues[I].Value, 0); If (SongInfo.Header.BPM <= 0) then begin Result := False; ErrorMessage := 'Can''t read BPM from Song'; end; end Else If (AValues[I].Name = 'RESOLUTION') then begin AValues[I].Value := Uppercase(AValues[I].Value); //Ultrastar Resolution is "how often a Beat is split / 4" If (AValues[I].Value = 'HEMIDEMISEMIQUAVER') then SongInfo.Header.Resolution := 64 div 4 Else If (AValues[I].Value = 'DEMISEMIQUAVER') then SongInfo.Header.Resolution := 32 div 4 Else If (AValues[I].Value = 'SEMIQUAVER') then SongInfo.Header.Resolution := 16 div 4 Else If (AValues[I].Value = 'QUAVER') then SongInfo.Header.Resolution := 8 div 4 Else If (AValues[I].Value = 'CROTCHET') then SongInfo.Header.Resolution := 4 div 4 Else begin //Can't understand teh Resolution :/ Result := False; ErrorMessage := 'Can''t read Resolution from Song'; end; end Else If (AValues[I].Name = 'GENRE') then begin SongInfo.Header.Genre := AValues[I].Value; end Else If (AValues[I].Name = 'YEAR') then begin SongInfo.Header.Year := AValues[I].Value; end Else If (AValues[I].Name = 'VERSION') then begin SingstarVersion := AValues[I].Value; end; end; ParserState := PS_Melody; //In Melody Tag end; end; PS_Melody: begin //Search for Sentence, Artist/Title Info or eo Melody If (Tag = 'SENTENCE') then begin ParserState := PS_Sentence; //Parse in a Sentence Tag now //Increase SentenceCount Inc(SongInfo.CountSentences); BindLyrics := True; //Don't let Txts Begin w/ Space //Search for Duett Singer Info MakeValuesArray; For I := 0 to High(AValues) do If (AValues[I].Name = 'SINGER') then begin AValues[I].Value := Uppercase(AValues[I].Value); If (AValues[I].Value = 'SOLO 1') then CurDuettSinger := DS_Player1 Else If (AValues[I].Value = 'SOLO 2') then CurDuettSinger := DS_Player2 Else CurDuettSinger := DS_Both; //In case of "Group" or anything that is not identified use Both end; end Else If (Tag = '!--') then begin //Comment, this may be Artist or Title Info I := Pos(':', Values); //Search for Delimiter If (I <> 0) then //If Found check for Title or Artist begin //Copy Title or Artist Tag to Tag String Tag := Uppercase(Trim(Copy(Values, 1, I - 1))); If (Tag = 'ARTIST') then begin SongInfo.Header.Artist := Trim(Copy(Values, I + 1, Length(Values) - I - 2)); Inc(SongInfo.ID); //Inc SongID when header Information is added end Else If (Tag = 'TITLE') then begin SongInfo.Header.Title := Trim(Copy(Values, I + 1, Length(Values) - I - 2)); Inc(SongInfo.ID); //Inc SongID when header Information is added end; end; end //Parsing for weird "Die toten Hosen" Tags Else If (Tag = '!--ARTIST:') OR (Tag = '!--ARTIST') then begin //Comment, with Artist Info I := Pos(':', Values); //Search for Delimiter Inc(SongInfo.ID); //Inc SongID when header Information is added SongInfo.Header.Artist := Trim(Copy(Values, I + 1, Length(Values) - I - 2)); end Else If (Tag = '!--TITLE:') OR (Tag = '!--TITLE') then begin //Comment, with Artist Info I := Pos(':', Values); //Search for Delimiter Inc(SongInfo.ID); //Inc SongID when header Information is added SongInfo.Header.Title := Trim(Copy(Values, I + 1, Length(Values) - I - 2)); end Else If (Tag = '/MELODY') then begin ParserState := PS_None; Exit; //Stop Parsing, Melody iTag ended end end; PS_Sentence: begin //Search for Notes or eo Sentence If (Tag = 'NOTE') then begin //Found Note //Get Values MakeValuesArray; NoteType := NT_Normal; For I := 0 to High(AValues) do begin If (AValues[I].Name = 'DURATION') then begin Duration := StrtoIntDef(AValues[I].Value, -1); If (Duration < 0) then begin Result := False; ErrorMessage := 'Can''t read duration from Note in Line: "' + Line + '"'; Exit; end; end Else If (AValues[I].Name = 'MIDINOTE') then begin Tone := StrtoIntDef(AValues[I].Value, 0); end Else If (AValues[I].Name = 'BONUS') AND (Uppercase(AValues[I].Value) = 'YES') then begin NoteType := NT_Golden; end Else If (AValues[I].Name = 'FREESTYLE') AND (Uppercase(AValues[I].Value) = 'YES') then begin NoteType := NT_Freestyle; end Else If (AValues[I].Name = 'LYRIC') then begin Lyric := AValues[I].Value; If (Length(Lyric) > 0) then begin If (Lyric = '-') then Lyric[1] := Settings.DashReplacement; If (not BindLyrics) then Lyric := ' ' + Lyric; If (Length(Lyric) > 2) AND (Lyric[Length(Lyric)-1] = ' ') AND (Lyric[Length(Lyric)] = '-') then begin //Between this and the next Lyric should be no space BindLyrics := True; SetLength(Lyric, Length(Lyric) - 2); end else BindLyrics := False; //There should be a Space end; end; end; //Add Note I := SongInfo.CountSentences - 1; If (Length(Lyric) > 0) then begin //Real note, no rest //First Note of Sentence If (Length(SongInfo.Sentences) < SongInfo.CountSentences) then begin SetLength(SongInfo.Sentences, SongInfo.CountSentences); SetLength(SongInfo.Sentences[I].Notes, 0); end; //First Note of Song -> Generate Gap If (FirstNote) then begin //Calculate Gap If (SongInfo.Header.Resolution <> 0) AND (SongInfo.Header.BPM <> 0) then SongInfo.Header.Gap := Round(CurPosinSong / (SongInfo.Header.BPM*SongInfo.Header.Resolution) * 60000) Else begin Result := False; ErrorMessage := 'Can''t calculate Gap, no Resolution or BPM present.'; Exit; end; CurPosinSong := 0; //Start at 0, because Gap goes until here Inc(SongInfo.ID); //Add Header Value therefore Inc FirstNote := False; end; J := Length(SongInfo.Sentences[I].Notes); SetLength(SongInfo.Sentences[I].Notes, J + 1); SongInfo.Sentences[I].Notes[J].Start := CurPosinSong; SongInfo.Sentences[I].Notes[J].Duration := Duration; SongInfo.Sentences[I].Notes[J].Tone := Tone; SongInfo.Sentences[I].Notes[J].NoteTyp := NoteType; SongInfo.Sentences[I].Notes[J].Lyric := Lyric; //Inc Pos in Song Inc(CurPosInSong, Duration); end else begin //just change pos in Song Inc(CurPosInSong, Duration); end; end Else If (Tag = '/SENTENCE') then begin //End of Sentence Tag ParserState := PS_Melody; //Delete Sentence if no Note is Added If (Length(SongInfo.Sentences) <> SongInfo.CountSentences) then begin SongInfo.CountSentences := Length(SongInfo.Sentences); end; end; end; end; end else //Empty Line -> parsed succesful ;) Result := true; end; Function TParser.ParseConfigForEdition(const Filename: IPath): String; var txt: TStringlist; Stream: TBinaryFileStream; I: Integer; J, K: Integer; S: String; begin Result := ''; Stream := TBinaryFileStream.Create(Filename, fmOpenRead); try txt := TStringlist.Create; txt.LoadFromStream(Stream); For I := 0 to txt.Count-1 do begin S := Trim(txt.Strings[I]); J := Pos('', S); If (J <> 0) then begin Inc(J, 14); K := Pos('', S); If (K