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; type TSingMode = ( smNormal, smPartyMode, smPlaylistRandom ); TBPM = record BPM: real; StartBeat: real; end; TScore = record Name: widestring; Score: integer; Length: string; end; TSong = class FileLineNo : integer; //Line which is readed at Last, for error reporting procedure ParseNote(LineNumber: integer; TypeP: char; StartP, DurationP, NoteP: integer; LyricS: string); procedure NewSentence(LineNumberP: integer; Param1, Param2: integer); function ReadTXTHeader( const aFileName : WideString ): boolean; public Path: widestring; Folder: widestring; // for sorting by folder fFileName, FileName: widestring; // sorting methods Category: array of widestring; // I think I won't need this Genre: widestring; Edition: widestring; Language: widestring; // 0.5.0: new Title: widestring; Artist: widestring; Text: widestring; Creator: widestring; Cover: widestring; CoverTex: TTexture; Mp3: widestring; Background: widestring; Video: widestring; VideoGAP: real; VideoLoaded: boolean; // 0.5.0: true if the video has been loaded NotesGAP: integer; Start: real; // in seconds Finish: integer; // in miliseconds Relative: boolean; Resolution: integer; BPM: array of TBPM; GAP: real; // in miliseconds 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 SongFile: TextFile; // all procedures in this unit operates on this file Base : array[0..1] of integer; Rel : array[0..1] of integer; Mult : integer; MultBPM : integer; constructor create ( const aFileName : WideString ); function LoadSong: boolean; function Analyse(): boolean; procedure clear(); end; implementation uses TextGL, UIni, UMusic, //needed for Lines UMain; //needed for Player constructor TSong.create( const aFileName : WideString ); begin Mult := 1; MultBPM := 4; fFileName := aFileName; if fileexists( aFileName ) then begin self.Path := ExtractFilePath( aFileName ); self.Folder := ExtractFilePath( aFileName ); self.FileName := ExtractFileName( aFileName ); (* if ReadTXTHeader( aFileName ) then begin LoadSong(); end else begin Log.LogError('Error Loading SongHeader, abort Song Loading'); Exit; end; *) end; end; function TSong.LoadSong(): boolean; var TempC: char; Text: string; CP: integer; // Current Player (0 or 1) Count: integer; Both: boolean; Param1: integer; Param2: integer; Param3: integer; ParamS: string; I: Integer; begin Result := false; if not FileExists(Path + PathDelim + FileName) then begin Log.LogError('File not found: "' + Path + PathDelim + FileName + '"', 'TSong.LoadSong()'); exit; end; MultBPM := 4; // multiply beat-count of note by 4 Mult := 1; // accuracy of measurement of note Base[0] := 100; // high number Lines[0].NoteType := 0; self.Relative := false; Rel[0] := 0; CP := 0; Both := false; if Length(Player) = 2 then Both := true; try // Open song file for reading..... FileMode := fmOpenRead; AssignFile(SongFile, fFileName); Reset(SongFile); //Clear old Song Header if (self.Path = '') then self.Path := ExtractFilePath(FileName); if (self.FileName = '') then self.Filename := ExtractFileName(FileName); Result := False; Reset(SongFile); FileLineNo := 0; //Search for Note Begining repeat ReadLn(SongFile, Text); Inc(FileLineNo); if (EoF(SongFile)) then begin //Song File Corrupted - No Notes CloseFile(SongFile); Log.LogError('Could not load txt File, no Notes found: ' + FileName); Result := False; Exit; end; Read(SongFile, TempC); until ((TempC = ':') or (TempC = 'F') or (TempC = '*')); SetLength(Lines, 2); for Count := 0 to High(Lines) do begin SetLength(Lines[Count].Line, 1); Lines[Count].High := 0; Lines[Count].Number := 1; Lines[Count].Current := 0; Lines[Count].Resolution := self.Resolution; Lines[Count].NotesGAP := self.NotesGAP; Lines[Count].Line[0].IlNut := 0; Lines[Count].Line[0].HighNote := -1; Lines[Count].Line[0].LastLine := False; end; // TempC := ':'; // TempC := Text[1]; // read from backup variable, don't use default ':' value while (TempC <> 'E') AND (not EOF(SongFile)) do begin if (TempC = ':') or (TempC = '*') or (TempC = 'F') then begin // read notes Read(SongFile, Param1); Read(SongFile, Param2); Read(SongFile, Param3); Read(SongFile, ParamS); //Check for ZeroNote if Param2 = 0 then Log.LogError('Found ZeroNote at "'+TempC+' '+IntToStr(Param1)+' '+IntToStr(Param2)+' '+IntToStr(Param3)+ParamS+'" -> Note ignored!') else begin // add notes if not Both then // P1 ParseNote(0, TempC, (Param1+Rel[0]) * Mult, Param2 * Mult, Param3, ParamS) else begin // P1 + P2 ParseNote(0, TempC, (Param1+Rel[0]) * Mult, Param2 * Mult, Param3, ParamS); ParseNote(1, TempC, (Param1+Rel[1]) * Mult, Param2 * Mult, Param3, ParamS); end; end; //Zeronote check end; // if if TempC = '-' then begin // reads sentence Read(SongFile, Param1); if self.Relative then Read(SongFile, Param2); // 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 if TempC = 'B' then begin SetLength(self.BPM, Length(self.BPM) + 1); Read(SongFile, self.BPM[High(self.BPM)].StartBeat); self.BPM[High(self.BPM)].StartBeat := self.BPM[High(self.BPM)].StartBeat + Rel[0]; Read(SongFile, Text); self.BPM[High(self.BPM)].BPM := StrToFloat(Text); self.BPM[High(self.BPM)].BPM := self.BPM[High(self.BPM)].BPM * Mult * MultBPM; end; if not Both then begin Lines[CP].Line[Lines[CP].High].BaseNote := Base[CP]; Lines[CP].Line[Lines[CP].High].LyricWidth := glTextWidth(PChar(Lines[CP].Line[Lines[CP].High].Lyric)); //Total Notes Patch Lines[CP].Line[Lines[CP].High].TotalNotes := 0; for I := low(Lines[CP].Line[Lines[CP].High].Note) to high(Lines[CP].Line[Lines[CP].High].Note) do begin Lines[CP].Line[Lines[CP].High].TotalNotes := Lines[CP].Line[Lines[CP].High].TotalNotes + Lines[CP].Line[Lines[CP].High].Note[I].Length * Lines[CP].Line[Lines[CP].High].Note[I].NoteType; end; //Total Notes Patch End end else begin for Count := 0 to High(Lines) do begin Lines[Count].Line[Lines[Count].High].BaseNote := Base[Count]; Lines[Count].Line[Lines[Count].High].LyricWidth := glTextWidth(PChar(Lines[Count].Line[Lines[Count].High].Lyric)); //Total Notes Patch Lines[Count].Line[Lines[Count].High].TotalNotes := 0; for I := low(Lines[Count].Line[Lines[Count].High].Note) to high(Lines[Count].Line[Lines[Count].High].Note) do begin Lines[Count].Line[Lines[Count].High].TotalNotes := Lines[Count].Line[Lines[Count].High].TotalNotes + Lines[Count].Line[Lines[Count].High].Note[I].Length * Lines[Count].Line[Lines[Count].High].Note[I].NoteType; end; //Total Notes Patch End end; end; Read(SongFile, TempC); Inc(FileLineNo); end; // while} for Count := 0 to High(Lines) do begin Lines[Count].Line[High(Lines[Count].Line)].LastLine := True; end; CloseFile(SongFile); except try CloseFile(SongFile); except end; Log.LogError('Error Loading File: "' + fFileName + '" in Line ' + inttostr(FileLineNo)); exit; end; Result := true; end; function TSong.ReadTXTHeader(const aFileName : WideString): boolean; function song_StrtoFloat( aValue : String ) : Extended; var lValue : String; // lOldDecimalSeparator : Char; // Auto Removed, Unused Variable begin lValue := aValue; if (Pos(',', lValue) <> 0) then lValue[Pos(',', lValue)] := '.'; Result := StrToFloatDef(lValue, 0); end; var Line, Identifier, Value: String; Temp : word; Done : byte; begin Result := true; Done := 0; //Read first Line ReadLn (SongFile, Line); if (Length(Line)<=0) then begin Log.LogError('File Starts with Empty Line: ' + aFileName); Result := False; Exit; end; //Read Lines while Line starts with # or its empty While ( Length(Line) = 0 ) OR ( Line[1] = '#' ) DO begin //Increase Line Number Inc (FileLineNo); Temp := Pos(':', Line); //Line has a Seperator-> Headerline if (Temp <> 0) then begin //Read Identifier and Value Identifier := Uppercase(Trim(Copy(Line, 2, Temp - 2))); //Uppercase is for Case Insensitive Checks Value := Trim(Copy(Line, Temp + 1,Length(Line) - Temp)); //Check the Identifier (If Value is given) if (Length(Value) <> 0) then begin //----------- //Required Attributes //----------- {$IFDEF UTF8_FILENAMES} if ((Identifier = 'MP3') or (Identifier = 'BACKGROUND') or (Identifier = 'COVER') or (Identifier = 'VIDEO')) then Value := Utf8Encode(Value); {$ENDIF} //Title if (Identifier = 'TITLE') then begin self.Title := Value; //Add Title Flag to Done Done := Done or 1; end //Artist else if (Identifier = 'ARTIST') then begin self.Artist := Value; //Add Artist Flag to Done Done := Done or 2; end //MP3 File //Test if Exists else if (Identifier = 'MP3') AND (FileExists(self.Path + Value)) then begin self.Mp3 := Value; //Add Mp3 Flag to Done Done := Done or 4; end //Beats per Minute else if (Identifier = 'BPM') then begin SetLength(self.BPM, 1); self.BPM[0].StartBeat := 0; self.BPM[0].BPM := song_StrtoFloat( 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 //--------- // Video Gap else if (Identifier = 'GAP') then self.GAP := song_StrtoFloat( Value ) //Cover Picture else if (Identifier = 'COVER') then self.Cover := Value //Background Picture else if (Identifier = 'BACKGROUND') then self.Background := Value // Video File else if (Identifier = 'VIDEO') then begin if (FileExists(self.Path + Value)) then self.Video := Value else Log.LogError('Can''t find Video File in Song: ' + aFileName); end // Video Gap else if (Identifier = 'VIDEOGAP') then self.VideoGAP := song_StrtoFloat( Value ) //Genre Sorting else if (Identifier = 'GENRE') then self.Genre := Value //Edition Sorting else if (Identifier = 'EDITION') then self.Edition := Value //Creator Tag else if (Identifier = 'CREATOR') then self.Creator := Value //Language Sorting else if (Identifier = 'LANGUAGE') then self.Language := Value // Song Start else if (Identifier = 'START') then self.Start := song_StrtoFloat( Value ) // Song Ending else if (Identifier = 'END') then TryStrtoInt(Value, self.Finish) // Resolution else if (Identifier = 'RESOLUTION') then TryStrtoInt(Value, self.Resolution) // Notes Gap else if (Identifier = 'NOTESGAP') then TryStrtoInt(Value, self.NotesGAP) // Relative Notes else if (Identifier = 'RELATIVE') AND (uppercase(Value) = 'YES') then self.Relative := True; end; end; if not EOf(SongFile) then ReadLn (SongFile, Line) else begin Result := False; Log.LogError('File Incomplete or not Ultrastar TxT (A): ' + aFileName); break; end; end; if self.Cover = '' then self.Cover := platform.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: ' + self.FileName) else if (Done and 4) = 0 then //No MP3 Flag Log.LogError('MP3 Tag/File Missing: ' + self.FileName) else if (Done and 2) = 0 then //No Artist Flag Log.LogError('Artist Tag Missing: ' + self.FileName) else if (Done and 1) = 0 then //No Title Flag Log.LogError('Title Tag Missing: ' + self.FileName) else //unknown Error Log.LogError('File Incomplete or not Ultrastar TxT (B - '+ inttostr(Done) +'): ' + aFileName); end; end; procedure TSong.ParseNote(LineNumber: integer; TypeP: char; StartP, DurationP, NoteP: integer; LyricS: string); //var // Space: boolean; // Auto Removed, Unused Variable begin case Ini.Solmization of 1: // european begin case (NoteP mod 12) of 0..1: LyricS := ' do '; 2..3: LyricS := ' re '; 4: LyricS := ' mi '; 5..6: LyricS := ' fa '; 7..8: LyricS := ' sol '; 9..10: LyricS := ' la '; 11: LyricS := ' si '; end; end; 2: // japanese begin case (NoteP mod 12) of 0..1: LyricS := ' do '; 2..3: LyricS := ' re '; 4: LyricS := ' mi '; 5..6: LyricS := ' fa '; 7..8: LyricS := ' so '; 9..10: LyricS := ' la '; 11: LyricS := ' shi '; end; end; 3: // american begin case (NoteP mod 12) of 0..1: LyricS := ' do '; 2..3: LyricS := ' re '; 4: LyricS := ' mi '; 5..6: LyricS := ' fa '; 7..8: LyricS := ' sol '; 9..10: LyricS := ' la '; 11: LyricS := ' ti '; end; end; end; // case with Lines[LineNumber].Line[Lines[LineNumber].High] do begin SetLength(Note, Length(Note) + 1); IlNut := IlNut + 1; HighNote := HighNote + 1; Melody.IlNut := Melody.IlNut + 1; Note[HighNote].Start := StartP; if IlNut = 1 then begin StartNote := Note[HighNote].Start; if Lines[LineNumber].Number = 1 then Start := -100; // Start := Note[HighNote].Start; end; Note[HighNote].Length := DurationP; Melody.NoteLength := Melody.NoteLength + Note[HighNote].Length; // back to the normal system with normal, golden and now freestyle notes case TypeP of 'F': Note[HighNote].NoteType := 0; ':': Note[HighNote].NoteType := 1; '*': Note[HighNote].NoteType := 2; end; Lines[LineNumber].NoteType := Lines[LineNumber].NoteType + Note[HighNote].Length * Note[HighNote].NoteType; Note[HighNote].Tone := NoteP; if Note[HighNote].Tone < Base[LineNumber] then Base[LineNumber] := Note[HighNote].Tone; Note[HighNote].ToneGamus := Note[HighNote].ToneGamus mod 12; Note[HighNote].Text := Copy(LyricS, 2, 100); Lyric := Lyric + Note[HighNote].Text; if TypeP = 'F' then Note[HighNote].FreeStyle := true; End_ := Note[HighNote].Start + Note[HighNote].Length; end; // with end; procedure TSong.NewSentence(LineNumberP: integer; Param1, Param2: integer); var I: Integer; begin // stara czesc //Alter Satz //Update Old Part Lines[LineNumberP].Line[Lines[LineNumberP].High].BaseNote := Base[LineNumberP]; Lines[LineNumberP].Line[Lines[LineNumberP].High].LyricWidth := glTextWidth(PChar(Lines[LineNumberP].Line[Lines[LineNumberP].High].Lyric)); //Total Notes Patch Lines[LineNumberP].Line[Lines[LineNumberP].High].TotalNotes := 0; for I := low(Lines[LineNumberP].Line[Lines[LineNumberP].High].Note) to high(Lines[LineNumberP].Line[Lines[LineNumberP].High].Note) do begin Lines[LineNumberP].Line[Lines[LineNumberP].High].TotalNotes := Lines[LineNumberP].Line[Lines[LineNumberP].High].TotalNotes + Lines[LineNumberP].Line[Lines[LineNumberP].High].Note[I].Length * Lines[LineNumberP].Line[Lines[LineNumberP].High].Note[I].NoteType; end; //Total Notes Patch End // nowa czesc //Neuer Satz //Update New Part SetLength(Lines[LineNumberP].Line, Lines[LineNumberP].Number + 1); Lines[LineNumberP].High := Lines[LineNumberP].High + 1; Lines[LineNumberP].Number := Lines[LineNumberP].Number + 1; Lines[LineNumberP].Line[Lines[LineNumberP].High].HighNote := -1; 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; Base[LineNumberP] := 100; // high number end; procedure TSong.clear(); begin //Main Information Title := ''; Artist := ''; //Sortings: Genre := 'Unknown'; Edition := 'Unknown'; Language := 'Unknown'; //Language Patch //Required Information Mp3 := ''; {$IFDEF FPC} setlength( BPM, 0 ); {$ELSE} BPM := nil; {$ENDIF} GAP := 0; Start := 0; Finish := 0; //Additional Information Background := ''; Cover := ''; Video := ''; VideoGAP := 0; NotesGAP := 0; Resolution := 4; Creator := ''; end; function TSong.Analyse(): boolean; begin Result := False; //Reset LineNo FileLineNo := 0; //Open File and set File Pointer to the beginning AssignFile(SongFile, self.Path + self.FileName); try Reset(SongFile); //Clear old Song Header self.clear; //Read Header Result := self.ReadTxTHeader( FileName ) //And Close File finally CloseFile(SongFile); end; end; end.