diff options
-rw-r--r-- | unicode/src/base/UCovers.pas | 6 | ||||
-rw-r--r-- | unicode/src/base/UFiles.pas | 136 | ||||
-rw-r--r-- | unicode/src/base/UPath.pas | 325 | ||||
-rw-r--r-- | unicode/src/base/UPlaylist.pas | 4 | ||||
-rw-r--r-- | unicode/src/base/USong.pas | 499 | ||||
-rw-r--r-- | unicode/src/base/UXMLSong.pas | 37 | ||||
-rw-r--r-- | unicode/src/media/UMediaCore_FFmpeg.pas | 111 |
7 files changed, 800 insertions, 318 deletions
diff --git a/unicode/src/base/UCovers.pas b/unicode/src/base/UCovers.pas index 0dbe672a..228a0783 100644 --- a/unicode/src/base/UCovers.pas +++ b/unicode/src/base/UCovers.pas @@ -211,9 +211,11 @@ end; procedure TCoverDatabase.Open(); var Version: integer; - Filename: IPath; + Filename, TmpPath: IPath; begin - Filename := Platform.GetGameUserPath().Append(COVERDB_FILENAME); + // TODO: remove fpc refcount workaround + TmpPath := Platform.GetGameUserPath(); + Filename := TmpPath.Append(COVERDB_FILENAME); DB := TSQLiteDatabase.Create(Filename.ToUTF8()); Version := GetVersion(); diff --git a/unicode/src/base/UFiles.pas b/unicode/src/base/UFiles.pas index 9996eeb1..a46d4e0d 100644 --- a/unicode/src/base/UFiles.pas +++ b/unicode/src/base/UFiles.pas @@ -105,84 +105,84 @@ var begin // Relative := true; // override (idea - use shift+S to save with relative) Result := ssrOK; - SongFile := nil; try - SongFile := TTextFileStream.Create(Name, fmCreate); - - if (Song.Encoding = encUTF8) then - SongFile.Write(UTF8_BOM); - - SongFile.WriteLine('#ENCODING:' + EncodingName(Song.Encoding)); - SongFile.WriteLine('#TITLE:' + EncodeToken(Song.Title)); - SongFile.WriteLine('#ARTIST:' + EncodeToken(Song.Artist)); - - if Song.Creator <> '' then SongFile.WriteLine('#CREATOR:' + EncodeToken(Song.Creator)); - if Song.Edition <> 'Unknown' then SongFile.WriteLine('#EDITION:' + EncodeToken(Song.Edition)); - if Song.Genre <> 'Unknown' then SongFile.WriteLine('#GENRE:' + EncodeToken(Song.Genre)); - if Song.Language <> 'Unknown' then SongFile.WriteLine('#LANGUAGE:' + EncodeToken(Song.Language)); - - SongFile.WriteLine('#MP3:' + EncodeToken(Song.Mp3.ToUTF8)); - if Song.Cover.IsSet then SongFile.WriteLine('#COVER:' + EncodeToken(Song.Cover.ToUTF8)); - if Song.Background.IsSet then SongFile.WriteLine('#BACKGROUND:' + EncodeToken(Song.Background.ToUTF8)); - if Song.Video.IsSet then SongFile.WriteLine('#VIDEO:' + EncodeToken(Song.Video.ToUTF8)); - - if Song.VideoGAP <> 0 then SongFile.WriteLine('#VIDEOGAP:' + FloatToStr(Song.VideoGAP)); - if Song.Resolution <> 4 then SongFile.WriteLine('#RESOLUTION:' + IntToStr(Song.Resolution)); - if Song.NotesGAP <> 0 then SongFile.WriteLine('#NOTESGAP:' + IntToStr(Song.NotesGAP)); - if Song.Start <> 0 then SongFile.WriteLine('#START:' + FloatToStr(Song.Start)); - if Song.Finish <> 0 then SongFile.WriteLine('#END:' + IntToStr(Song.Finish)); - if Relative then SongFile.WriteLine('#RELATIVE:yes'); - - SongFile.WriteLine('#BPM:' + FloatToStr(Song.BPM[0].BPM / 4)); - SongFile.WriteLine('#GAP:' + FloatToStr(Song.GAP)); - - RelativeSubTime := 0; - for B := 1 to High(Song.BPM) do - SongFile.WriteLine('B ' + FloatToStr(Song.BPM[B].StartBeat) + ' ' - + FloatToStr(Song.BPM[B].BPM/4)); - - for C := 0 to Lines.High do - begin - for N := 0 to Lines.Line[C].HighNote do + SongFile := TMemTextFileStream.Create(Name, fmCreate); + try + if (Song.Encoding = encUTF8) then + SongFile.WriteString(UTF8_BOM); + + SongFile.WriteLine('#ENCODING:' + EncodingName(Song.Encoding)); + SongFile.WriteLine('#TITLE:' + EncodeToken(Song.Title)); + SongFile.WriteLine('#ARTIST:' + EncodeToken(Song.Artist)); + + if Song.Creator <> '' then SongFile.WriteLine('#CREATOR:' + EncodeToken(Song.Creator)); + if Song.Edition <> 'Unknown' then SongFile.WriteLine('#EDITION:' + EncodeToken(Song.Edition)); + if Song.Genre <> 'Unknown' then SongFile.WriteLine('#GENRE:' + EncodeToken(Song.Genre)); + if Song.Language <> 'Unknown' then SongFile.WriteLine('#LANGUAGE:' + EncodeToken(Song.Language)); + + SongFile.WriteLine('#MP3:' + EncodeToken(Song.Mp3.ToUTF8)); + if Song.Cover.IsSet then SongFile.WriteLine('#COVER:' + EncodeToken(Song.Cover.ToUTF8)); + if Song.Background.IsSet then SongFile.WriteLine('#BACKGROUND:' + EncodeToken(Song.Background.ToUTF8)); + if Song.Video.IsSet then SongFile.WriteLine('#VIDEO:' + EncodeToken(Song.Video.ToUTF8)); + + if Song.VideoGAP <> 0 then SongFile.WriteLine('#VIDEOGAP:' + FloatToStr(Song.VideoGAP)); + if Song.Resolution <> 4 then SongFile.WriteLine('#RESOLUTION:' + IntToStr(Song.Resolution)); + if Song.NotesGAP <> 0 then SongFile.WriteLine('#NOTESGAP:' + IntToStr(Song.NotesGAP)); + if Song.Start <> 0 then SongFile.WriteLine('#START:' + FloatToStr(Song.Start)); + if Song.Finish <> 0 then SongFile.WriteLine('#END:' + IntToStr(Song.Finish)); + if Relative then SongFile.WriteLine('#RELATIVE:yes'); + + SongFile.WriteLine('#BPM:' + FloatToStr(Song.BPM[0].BPM / 4)); + SongFile.WriteLine('#GAP:' + FloatToStr(Song.GAP)); + + RelativeSubTime := 0; + for B := 1 to High(Song.BPM) do + SongFile.WriteLine('B ' + FloatToStr(Song.BPM[B].StartBeat) + ' ' + + FloatToStr(Song.BPM[B].BPM/4)); + + for C := 0 to Lines.High do begin - with Lines.Line[C].Note[N] do + for N := 0 to Lines.Line[C].HighNote do begin - //Golden + Freestyle Note Patch - case Lines.Line[C].Note[N].NoteType of - ntFreestyle: NoteState := 'F '; - ntNormal: NoteState := ': '; - ntGolden: NoteState := '* '; - end; // case - S := NoteState + IntToStr(Start-RelativeSubTime) + ' ' - + IntToStr(Length) + ' ' - + IntToStr(Tone) + ' ' - + EncodeToken(Text); - - SongFile.WriteLine(S); - end; // with - end; // N - - if C < Lines.High then // don't write end of last sentence - begin - if not Relative then - S := '- ' + IntToStr(Lines.Line[C+1].Start) - else + with Lines.Line[C].Note[N] do + begin + //Golden + Freestyle Note Patch + case Lines.Line[C].Note[N].NoteType of + ntFreestyle: NoteState := 'F '; + ntNormal: NoteState := ': '; + ntGolden: NoteState := '* '; + end; // case + S := NoteState + IntToStr(Start-RelativeSubTime) + ' ' + + IntToStr(Length) + ' ' + + IntToStr(Tone) + ' ' + + EncodeToken(Text); + + SongFile.WriteLine(S); + end; // with + end; // N + + if C < Lines.High then // don't write end of last sentence begin - S := '- ' + IntToStr(Lines.Line[C+1].Start - RelativeSubTime) + - ' ' + IntToStr(Lines.Line[C+1].Start - RelativeSubTime); - RelativeSubTime := Lines.Line[C+1].Start; + if not Relative then + S := '- ' + IntToStr(Lines.Line[C+1].Start) + else + begin + S := '- ' + IntToStr(Lines.Line[C+1].Start - RelativeSubTime) + + ' ' + IntToStr(Lines.Line[C+1].Start - RelativeSubTime); + RelativeSubTime := Lines.Line[C+1].Start; + end; + SongFile.WriteLine(S); end; - SongFile.WriteLine(S); - end; - end; // C + end; // C - SongFile.WriteLine('E'); + SongFile.WriteLine('E'); + finally + SongFile.Free; + end; except Result := ssrFileError; end; - - SongFile.Free; end; end. diff --git a/unicode/src/base/UPath.pas b/unicode/src/base/UPath.pas index fc8c028e..c74d1a16 100644 --- a/unicode/src/base/UPath.pas +++ b/unicode/src/base/UPath.pas @@ -45,6 +45,15 @@ type IPath = interface; {** + * TUnicodeMemoryStream + *} + TUnicodeMemoryStream = class(TMemoryStream) + public + procedure LoadFromFile(const FileName: IPath); + procedure SaveToFile(const FileName: IPath); + end; + + {** * TBinaryFileStream (inherited from THandleStream) *} {$IFDEF MSWINDOWS} @@ -60,22 +69,61 @@ type end; {** - * TUnicodeMemoryStream + * TTextFileStream *} - TUnicodeMemoryStream = class(TMemoryStream) + TTextFileStream = class(TStream) + protected + fLineBreak: RawByteString; + fFilename: IPath; + fMode: word; + + function ReadLine(var Success: boolean): RawByteString; overload; virtual; abstract; public - procedure LoadFromFile(const FileName: IPath); - procedure SaveToFile(const FileName: IPath); - end; + constructor Create(Filename: IPath; Mode: Word); - TTextFileStream = class(TBinaryFileStream) + function ReadString(): RawByteString; virtual; abstract; function ReadLine(var Line: UTF8String): boolean; overload; function ReadLine(var Line: AnsiString): boolean; overload; - procedure Write(Str: RawByteString); - procedure WriteLine(Line: RawByteString = ''); + + procedure WriteString(const Str: RawByteString); virtual; + procedure WriteLine(const Line: RawByteString); virtual; + + property LineBreak: RawByteString read fLineBreak write fLineBreak; + property Filename: IPath read fFilename; end; {** + * TMemTextStream + *} + TMemTextFileStream = class(TTextFileStream) + private + fStream: TMemoryStream; + protected + function GetSize: Int64; override; + + {** + * Copies fStream.Memory from StartPos to EndPos-1 to the result string; + *} + function CopyMemString(StartPos: int64; EndPos: int64): RawByteString; + public + constructor Create(Filename: IPath; Mode: Word); + destructor Destroy(); override; + + function Read(var Buffer; Count: Longint): Longint; override; + function Write(const Buffer; Count: Longint): Longint; override; + function Seek(Offset: Longint; Origin: Word): Longint; override; + function Seek(const Offset: Int64; Origin: TSeekOrigin): Int64; override; + + function ReadLine(var Success: boolean): RawByteString; override; + function ReadString(): RawByteString; override; + end; + + {** + TUnicodeIniStream = class() + end; + *} + + {** * pdKeep: Keep path as is, neither remove or append a delimiter * pdAppend: Append a delimiter if path does not have a trailing one * pdRemove: Remove a trailing delimiter from the path @@ -85,8 +133,44 @@ type IPathDynArray = array of IPath; {** - * IPath - * The Path's pathname is immutable and cannot be changed after creation. + * An IPath represents a filename, a directory or a filesystem path in general. + * It hides some of the operating system's specifics like path delimiters + * and encodings and provides an easy to use interface to handle them. + * Internally all paths are stored with the same path delimiter (PathDelim) + * and encoding (UTF-8). The transformation is already done AT THE CREATION of + * the IPath and hence calls to e.g. IPath.Equal() will not distinguish between + * Unix and Windows style paths. + * + * Create new paths with one of the Path() functions. + * If you need a string representation use IPath.ToNative/ToUTF8/ToWide. + * Note that due to the path-delimiter and encoding transformation the string + * might have changed. Path('one\test/path').ToUTF8() might return 'one/test/path'. + * + * It is recommended to use an IPath as long as possible without a string + * conversion (IPath.To...()). The whole Delphi (< 2009) and FPC RTL is ANSI + * only on Windows. If you would use for example FileExists(MyPath.ToNative) + * it would not find a file which contains characters that are not in the + * current locale. Same applies to AssignFile(), TFileStream.Create() and + * everything else in the RTL that expects a filename. + * As a rule of thumb: NEVER use any of the Delphi/FPC RTL filename functions + * if the filename parameter is not of a UTF8String or WideString type. + * + * If you need to open a file use TBinaryStream or TFileStream instead. Many + * of the RTL classes offer a LoadFromStream() method so ANSI Open() methods + * can be workaround. + * + * If there is only a ANSI and no IPath/UTF-8/WideString version and you cannot + * even pass a stream instead of a filename be aware that even if you know that + * a filename is ASCII only, subdirectories in an absolute path might contain + * some non-ASCII characters (for example the user's name) and hence might + * fail (if the characters are not in the current locale). + * It is rare but it happens. + * + * IMPORTANT: + * This interface needs the cwstring unit on Unix (Max OS X / Linux) systems. + * Cwstring functions (WideUpperCase, ...) cannot be used by external threads + * as FPC uses Thread-Local-Storage for the implementation. As a result do not + * call IPath stuff by external threads (e.g. in C callbacks or by SDL-threads). *} IPath = interface ['{686BF103-CE43-4598-B85D-A2C3AF950897}'] @@ -271,6 +355,7 @@ function PATH_NONE(): IPath; implementation uses + RTLConsts, UFilesystem; type @@ -426,6 +511,8 @@ end; function TPathImpl.ToUTF8(UseNativeDelim: boolean): UTF8String; begin + AssertRefCount; + if (UseNativeDelim) then Result := fName else @@ -583,20 +670,26 @@ end; function TPathImpl.Append(const Child: RawByteString; DelimOption: TPathDelimOption): IPath; begin + AssertRefCount; Result := Append(Path(Child), DelimOption); end; function TPathImpl.Append(const Child: WideString; DelimOption: TPathDelimOption): IPath; begin + AssertRefCount; Result := Append(Path(Child), DelimOption); end; function TPathImpl.Equals(const Other: IPath; IgnoreCase: boolean): boolean; var SelfPath, OtherPath: UTF8String; + TmpPath: IPath; begin - SelfPath := Self.GetAbsolutePath().RemovePathDelim().ToUTF8(); - OtherPath := Other.GetAbsolutePath().RemovePathDelim().ToUTF8(); + // TODO: remove fpc refcount bug workaround + TmpPath := Self.GetAbsolutePath(); + SelfPath := TmpPath.RemovePathDelim().ToUTF8(); + TmpPath := Other.GetAbsolutePath(); + OtherPath := TmpPath.RemovePathDelim().ToUTF8(); if (FileSystem.IsCaseSensitive() and not IgnoreCase) then Result := (CompareStr(SelfPath, OtherPath) = 0) else @@ -894,26 +987,220 @@ begin {$ENDIF} end; -{ TTextFileStream } +{ TTextStream } + +constructor TTextFileStream.Create(Filename: IPath; Mode: Word); +begin + inherited Create(); + fMode := Mode; + fFilename := Filename; + fLineBreak := sLineBreak; +end; function TTextFileStream.ReadLine(var Line: UTF8String): boolean; begin - // TODO + Line := ReadLine(Result); end; function TTextFileStream.ReadLine(var Line: AnsiString): boolean; begin - // TODO + Line := ReadLine(Result); end; -procedure TTextFileStream.WriteLine(Line: RawByteString); +procedure TTextFileStream.WriteString(const Str: RawByteString); +begin + WriteBuffer(Str[1], Length(Str)); +end; + +procedure TTextFileStream.WriteLine(const Line: RawByteString); +begin + WriteBuffer(Line[1], Length(Line)); + WriteBuffer(fLineBreak[1], Length(fLineBreak)); +end; + +{ TMemTextStream } + +constructor TMemTextFileStream.Create(Filename: IPath; Mode: Word); +var + FileStream: TBinaryFileStream; begin - Self.WriteBuffer(Line[1], Length(Line)); + inherited Create(Filename, Mode); + + fStream := TMemoryStream.Create(); + + // load data to memory in read mode + if ((Mode and 3) in [fmOpenRead, fmOpenReadWrite]) then + begin + FileStream := TBinaryFileStream.Create(Filename, fmOpenRead); + try + fStream.LoadFromStream(FileStream); + finally + FileStream.Free; + end; + end + // check if file exists for write-mode + else if ((Mode and 3) = fmOpenWrite) and (not Filename.IsFile) then + begin + raise EFOpenError.CreateResFmt(@SFOpenError, + [FileName.GetAbsolutePath.ToNative]); + end; end; -procedure TTextFileStream.Write(Str: RawByteString); +destructor TMemTextFileStream.Destroy(); +var + FileStream: TBinaryFileStream; + SaveMode: word; begin - Self.WriteBuffer(Str[1], Length(Str)); + // save changes in write mode (= not read-only mode) + if ((fMode and 3) <> fmOpenRead) then + begin + if (fMode = fmCreate) then + SaveMode := fmCreate + else + SaveMode := fmOpenWrite; + FileStream := TBinaryFileStream.Create(fFilename, SaveMode); + try + fStream.SaveToStream(FileStream); + finally + FileStream.Free; + end; + end; + + fStream.Free; + inherited; +end; + +function TMemTextFileStream.GetSize: Int64; +begin + Result := fStream.Size; +end; + +function TMemTextFileStream.Read(var Buffer; Count: Longint): Longint; +begin + Result := fStream.Read(Buffer, Count); +end; + +function TMemTextFileStream.Write(const Buffer; Count: Longint): Longint; +begin + Result := fStream.Write(Buffer, Count); +end; + +function TMemTextFileStream.Seek(Offset: Longint; Origin: Word): Longint; +begin + Result := fStream.Seek(Offset, Origin); +end; + +function TMemTextFileStream.Seek(const Offset: Int64; Origin: TSeekOrigin): Int64; +begin + Result := fStream.Seek(Offset, Origin); +end; + +function TMemTextFileStream.CopyMemString(StartPos: int64; EndPos: int64): RawByteString; +var + LineLength: cardinal; + Temp: RawByteString; +begin + LineLength := EndPos - StartPos; + if (LineLength > 0) then + begin + // set string length to line-length (+ zero-terminator) + SetLength(Temp, LineLength); + StrLCopy(PAnsiChar(Temp), + @PAnsiChar(fStream.Memory)[StartPos], + LineLength); + Result := Temp; + end + else + begin + Result := ''; + end; +end; + +function TMemTextFileStream.ReadString(): RawByteString; +var + TextPtr: PAnsiChar; + CurPos, StartPos, FileSize: Int64; +begin + TextPtr := PAnsiChar(fStream.Memory); + CurPos := Position; + FileSize := Size; + StartPos := -1; + + while (CurPos < FileSize) do + begin + // check for whitespace (tab, lf, cr, space) + if (TextPtr[CurPos] in [#9, #10, #13, ' ']) then + begin + // check if we are at the end of a string + if (StartPos > -1) then + Break; + end + else if (StartPos = -1) then // start of string found + begin + StartPos := CurPos; + end; + Inc(CurPos); + end; + + if (StartPos = -1) then + Result := '' + else + begin + Result := CopyMemString(StartPos, CurPos); + fStream.Position := CurPos; + end; +end; + +{* + * Implementation of ReadLine(). We need separate versions for UTF8String + * and AnsiString as "var" parameter types have to fit exactly. + * To avoid a var-parameter here, the internal version the Line parameter is + * used as return value. + *} +function TMemTextFileStream.ReadLine(var Success: boolean): RawByteString; +var + TextPtr: PAnsiChar; + CurPos, FileSize: int64; +begin + TextPtr := PAnsiChar(fStream.Memory); + CurPos := fStream.Position; + FileSize := Size; + + // check for EOF + if (CurPos >= FileSize) then + begin + Result := ''; + Success := false; + Exit; + end; + + Success := true; + + while (CurPos < FileSize) do + begin + if (TextPtr[CurPos] in [#10, #13]) then + begin + // copy text line + Result := CopyMemString(fStream.Position, CurPos); + + // handle windows style #13#10 (\r\n) newlines + if (TextPtr[CurPos] = #13) and + (CurPos+1 < FileSize) and + (TextPtr[CurPos+1] = #10) then + begin + Inc(CurPos); + end; + + // update stream pos + fStream.Position := CurPos+1; + + Exit; + end; + Inc(CurPos); + end; + + Result := CopyMemString(fStream.Position, CurPos); + fStream.Position := FileSize; end; { TUnicodeMemoryStream } diff --git a/unicode/src/base/UPlaylist.pas b/unicode/src/base/UPlaylist.pas index 09c196ef..03ae2ffb 100644 --- a/unicode/src/base/UPlaylist.pas +++ b/unicode/src/base/UPlaylist.pas @@ -184,7 +184,7 @@ begin //Load File try FilenameAbs := PlaylistPath.Append(Filename); - TextStream := TTextFileStream.Create(FilenameAbs, fmOpenRead); + TextStream := TMemTextFileStream.Create(FilenameAbs, fmOpenRead); except begin Log.LogError('Could not load Playlist: ' + FilenameAbs.ToNative); @@ -260,7 +260,7 @@ begin Exit; // open file for rewriting - TextStream := TTextFileStream.Create(PlaylistFile, fmCreate); + TextStream := TMemTextFileStream.Create(PlaylistFile, fmCreate); try // Write version (not nessecary but helpful) TextStream.WriteLine('######################################'); diff --git a/unicode/src/base/USong.pas b/unicode/src/base/USong.pas index 203ff517..cf9e7cdf 100644 --- a/unicode/src/base/USong.pas +++ b/unicode/src/base/USong.pas @@ -57,6 +57,7 @@ uses {$ENDIF} UCatCovers, UXMLSong, + UUnicodeUtils, UTextEncoding, UFilesystem, UPath; @@ -79,12 +80,18 @@ type private FileLineNo : integer; // line, which is read last, for error reporting - function EncodeFilename(Filename: string): string; + 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 ReadTXTHeader(const aFileName: IPath): boolean; + 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; @@ -133,8 +140,6 @@ type 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 operate on this file - Base : array[0..1] of integer; Rel : array[0..1] of integer; Mult : integer; @@ -253,33 +258,146 @@ begin Result := PATH_NONE; end; -function TSong.EncodeFilename(Filename: string): string; +function TSong.DecodeFilename(Filename: RawByteString): IPath; begin - {$IFDEF UTF8_FILENAMES} - Result := RecodeStringUTF8(Filename, Encoding); - {$ELSE} - // FIXME: just for compatibility, should be UTF-8 in general - if (Encoding = encUTF8) then - Result := UTF8ToAnsi(Filename) + 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 - Result := Filename; - {$ENDIF} + 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 - TempC: AnsiChar; - Text: UTF8String; + CurLine: RawByteString; + LinePos: integer; Count: integer; Both: boolean; - Param1: integer; - Param2: integer; - Param3: integer; - ParamS: UTF8String; + + Param0: AnsiChar; + Param1: integer; + Param2: integer; + Param3: integer; + ParamLyric: UTF8String; + I: integer; NotesFound: boolean; - TextStream: TTextFileStream; + SongFile: TTextFileStream; FileNamePath: IPath; begin Result := false; @@ -301,169 +419,159 @@ begin if Length(Player) = 2 then Both := true; - {$MESSAGE warn 'TODO: TSong.LoadSong'} - - (* try // Open song file for reading..... - TextStream := nil; - TextStream := TBinaryFileStream.Create(FullFileName, fmOpenRead); - - //Clear old Song Header - if (Self.Path.IsUnset) then - Self.Path := FullFileName.GetPath(); - - if (Self.FileName.IsUnset) then - Self.Filename := FullFileName.GetName(); - - //Search for Note Begining - FileLineNo := 0; - NotesFound := false; - while (TextStream.ReadLine(Text)) do - begin - Inc(FileLineNo); - if (Length(Text) > 0) and (Text[0] in [':', 'F', '*']) then + SongFile := TMemTextFileStream.Create(FileNamePath, fmOpenRead); + try + //Search for Note Beginning + FileLineNo := 0; + NotesFound := false; + while (SongFile.ReadLine(CurLine)) do begin - TempC := Text[0]; - NotesFound := true; - Break; + Inc(FileLineNo); + if (Length(CurLine) > 0) and (CurLine[1] in [':', 'F', '*']) then + begin + NotesFound := true; + Break; + end; end; - end; - - if (not NotesFound) then - begin //Song File Corrupted - No Notes - TextStream.Free; - Log.LogError('Could not load txt File, no Notes found: ' + FileName); - 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 (TempC <> 'E') and (not EOF(SongFile)) do - begin + 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; - if (TempC in [':', '*', 'F']) then + SetLength(Lines, 2); + for Count := 0 to High(Lines) do 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 - - else 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 + 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; - else if TempC = 'B' then + while true do 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]; + LinePos := 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; - - ReadLn(SongFile); //Jump to next line in File, otherwise the next Read would catch the linebreak(e.g. #13 #10 on win32) + 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 ZeroNote 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]; - Read(SongFile, TempC); - Inc(FileLineNo); - end; // while + self.BPM[High(self.BPM)].BPM := ParseLyricFloatParam(CurLine, LinePos); + self.BPM[High(self.BPM)].BPM := self.BPM[High(self.BPM)].BPM * Mult * MultBPM; + end; - CloseFile(SongFile); + // Read next line in File + if (not SongFile.ReadLine(CurLine)) then + Break; - for I := 0 to High(Lines) do + Inc(FileLineNo); + end; // while + finally + SongFile.Free; + end; + except + on E: Exception 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: "' + FullFileName + '"'); - 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: ' + Filename); - end; - end; + Log.LogError(Format('Error Loading File: "%s" in Line %d,%d: %s', + [FileNamePath.ToNative, FileLineNo, LinePos, E.Message])); + Exit; end; + end; - for Count := 0 to High(Lines) do + for I := 0 to High(Lines) do + begin + if ((Both) or (I = 0)) then begin - if (High(Lines[Count].Line) >= 0) then - Lines[Count].Line[High(Lines[Count].Line)].LastLine := true; - end; - except - try - CloseFile(SongFile); - except + if (Length(Lines[I].Line) < 2) then + begin + LastError := 'ERROR_CORRUPT_SONG_NO_BREAKS'; + Log.LogError('Error Loading File, Can''t find any Linebreaks: "' + 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; - LastError := 'ERROR_CORRUPT_SONG_ERROR_IN_LINE'; - Log.LogError('Error Loading File: "' + FullFileName + '" in Line ' + inttostr(FileLineNo)); - exit; + 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; @@ -724,27 +832,25 @@ begin Result := StrToFloatDef(TempValue, 0); end; -function TSong.ReadTXTHeader(const aFileName : IPath): boolean; +function TSong.ReadTXTHeader(SongFile: TTextFileStream): boolean; var Line, Identifier: string; Value: string; SepPos: integer; // separator position Done: byte; // bit-vector of mandatory fields - EncFile: string; // encoded filename + EncFile: IPath; // encoded filename + FullFileName: string; begin Result := true; Done := 0; - {$message warn 'TSong.ReadTXTHeader'} - - (* + FullFileName := Path.Append(Filename).ToNative; //Read first Line - ReadLn (SongFile, Line); - + SongFile.ReadLine(Line); if (Length(Line) <= 0) then begin - Log.LogError('File Starts with Empty Line: ' + Path + PathDelim + aFileName, + Log.LogError('File Starts with Empty Line: ' + FullFileName, 'TSong.ReadTXTHeader'); Result := false; Exit; @@ -772,7 +878,7 @@ begin //Check the Identifier (If Value is given) if (Length(Value) = 0) then begin - Log.LogError('Empty field "'+Identifier+'" in file ' + Path + PathDelim + aFileName, + Log.LogError('Empty field "'+Identifier+'" in file ' + FullFileName, 'TSong.ReadTXTHeader'); end else @@ -799,8 +905,8 @@ begin //MP3 File else if (Identifier = 'MP3') then begin - EncFile := EncodeFilename(Value); - if (FileExists(self.Path + EncFile)) then + EncFile := DecodeFilename(Value); + if (Self.Path.Append(EncFile).IsFile) then begin self.Mp3 := EncFile; @@ -837,23 +943,23 @@ begin //Cover Picture else if (Identifier = 'COVER') then begin - self.Cover := EncodeFilename(Value); + self.Cover := DecodeFilename(Value); end //Background Picture else if (Identifier = 'BACKGROUND') then begin - self.Background := EncodeFilename(Value); + self.Background := DecodeFilename(Value); end // Video File else if (Identifier = 'VIDEO') then begin - EncFile := EncodeFilename(Value); - if (FileExists(self.Path + EncFile)) then + EncFile := DecodeFilename(Value); + if (self.Path.Append(EncFile).IsFile) then self.Video := EncFile else - Log.LogError('Can''t find Video File in Song: ' + aFileName); + Log.LogError('Can''t find Video File in Song: ' + FullFileName); end // Video Gap @@ -925,37 +1031,33 @@ begin end; // End check for non-empty Value - // check for end of file - if Eof(SongFile) then + // read next line + if (not SongFile.ReadLine(Line)) then begin Result := false; - Log.LogError('File Incomplete or not Ultrastar TxT (A): ' + aFileName); + Log.LogError('File Incomplete or not Ultrastar TxT (A): ' + FullFileName); Break; end; - - // read next line - ReadLn(SongFile, Line) end; // while - if self.Cover = '' then - self.Cover := platform.FindSongFile(Path, '*[CO].jpg'); + 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: ' + self.FileName) + Log.LogError('BPM Tag Missing: ' + FullFileName) else if (Done and 4) = 0 then //No MP3 Flag - Log.LogError('MP3 Tag/File Missing: ' + self.FileName) + Log.LogError('MP3 Tag/File Missing: ' + FullFileName) else if (Done and 2) = 0 then //No Artist Flag - Log.LogError('Artist Tag Missing: ' + self.FileName) + Log.LogError('Artist Tag Missing: ' + FullFileName) else if (Done and 1) = 0 then //No Title Flag - Log.LogError('Title Tag Missing: ' + self.FileName) + Log.LogError('Title Tag Missing: ' + FullFileName) else //unknown Error - Log.LogError('File Incomplete or not Ultrastar TxT (B - '+ inttostr(Done) +'): ' + aFileName); + Log.LogError('File Incomplete or not Ultrastar TxT (B - '+ inttostr(Done) +'): ' + FullFileName); end; - *) end; function TSong.GetErrorLineNo: integer; @@ -1049,13 +1151,6 @@ begin if Note[HighNote].Tone < BaseNote then BaseNote := Note[HighNote].Tone; - //delete the space that seperates the notes pitch from its lyrics - //it is left in the LyricS string because Read("some ordinal type") will - //set the files pointer to the first whitespace character after the - //ordinal string. Trim is no solution because it would cut the spaces - //that seperate the words of the lyrics, too. - Delete(LyricS, 1, 1); - DecodeStringUTF8(LyricS, Note[HighNote].Text, Encoding); Lyric := Lyric + Note[HighNote].Text; @@ -1148,15 +1243,13 @@ begin FileLineNo := 0; //Open File and set File Pointer to the beginning - SongFile := TTextFileStream.Create(Self.Path.Append(Self.FileName), fmOpenRead); + SongFile := TMemTextFileStream.Create(Self.Path.Append(Self.FileName), fmOpenRead); try //Clear old Song Header Self.clear; //Read Header - Result := self.ReadTxTHeader(FileName) - - //And Close File + Result := Self.ReadTxTHeader(SongFile) finally SongFile.Free; end; diff --git a/unicode/src/base/UXMLSong.pas b/unicode/src/base/UXMLSong.pas index e8962539..e9751eba 100644 --- a/unicode/src/base/UXMLSong.pas +++ b/unicode/src/base/UXMLSong.pas @@ -134,18 +134,14 @@ var I: Integer; FileStream: TBinaryFileStream; begin - {$message warn 'TODO: TParser.ParseSong'} - -(* Result := False; if Filename.IsFile() then begin - SSFile := TStringList.Create; - ErrorMessage := 'Can''t open melody.xml file'; + SSFile := TStringList.Create; + FileStream := TBinaryFileStream.Create(Filename, fmOpenRead); try - FileStream := TBinaryFileStream.Create(Filename); SSFile.LoadFromStream(FileStream); ErrorMessage := ''; @@ -176,25 +172,24 @@ begin finally SSFile.Free; + FileStream.Free; end; end; - *) end; function TParser.ParseSongHeader (const Filename: IPath): Boolean; -var I: Integer; +var + I: Integer; + Stream: TBinaryFileStream; begin Result := False; - {$message warn 'TODO: TParser.ParseSong'} - (* - if FileExists(Filename) then + if Filename.IsFile() then begin SSFile := TStringList.Create; - SSFile.Clear; - + Stream := TBinaryFileStream.Create(Filename, fmOpenRead); try - SSFile.LoadFromFile(Filename); + SSFile.LoadFromStream(Stream); If (SSFile.Count > 0) then begin @@ -225,11 +220,11 @@ begin finally SSFile.Free; + Stream.Free; end; end else ErrorMessage := 'Can''t find melody.xml file'; - *) end; Function TParser.ParseLine(Line: String): Boolean; @@ -591,19 +586,17 @@ end; Function TParser.ParseConfigForEdition(const Filename: IPath): String; var txt: TStringlist; + Stream: TBinaryFileStream; I: Integer; J, K: Integer; S: String; begin Result := ''; - txt := TStringlist.Create; - {$message warn 'TODO: TParser.ParseConfigForEdition'} - - (* + Stream := TBinaryFileStream.Create(Filename, fmOpenRead); try - txt.LoadFromFile(Filename); - + txt := TStringlist.Create; + txt.LoadFromStream(Stream); For I := 0 to txt.Count-1 do begin S := Trim(txt.Strings[I]); @@ -623,8 +616,8 @@ begin Edition := Result; finally txt.Free; + Stream.Free; end; - *) end; end. diff --git a/unicode/src/media/UMediaCore_FFmpeg.pas b/unicode/src/media/UMediaCore_FFmpeg.pas index 9ad19a5b..5f56b467 100644 --- a/unicode/src/media/UMediaCore_FFmpeg.pas +++ b/unicode/src/media/UMediaCore_FFmpeg.pas @@ -34,12 +34,16 @@ interface {$I switches.inc} uses - UMusic, + Classes, + ctypes, + sdl, avcodec, avformat, avutil, + avio, + UMusic, ULog, - sdl; + UPath; type PPacketQueue = ^TPacketQueue; @@ -220,6 +224,109 @@ begin Result := true; end; +function FFmpegStreamRead(Opaque: Pointer; Buffer: PByteArray; BufSize: cint): cint; cdecl; +var + Stream: TStream; +begin + Stream := TStream(Opaque); + if (Stream = nil) then + raise EInvalidContainer.Create('FFmpegStreamRead on nil'); + try + Result := Stream.Read(Buffer[0], BufSize); + except + Result := -1; + end; +end; + +function FFmpegStreamWrite(Opaque: Pointer; Buffer: PByteArray; BufSize: cint): cint; cdecl; +var + Stream: TStream; +begin + Stream := TStream(Opaque); + if (Stream = nil) then + raise EInvalidContainer.Create('FFmpegStreamWrite on nil'); + try + Result := Stream.Write(Buffer[0], BufSize); + except + Result := -1; + end; +end; + +function FFmpegStreamSeek(Opaque: Pointer; Offset: cint64; Whence: cint): cint64; cdecl; +var + Stream : TStream; + Origin : Word; +begin + Stream := TStream(opaque); + if (Stream = nil) then + raise EInvalidContainer.Create('FFmpegStreamSeek on nil'); + case whence of + 0 : Origin := soFromBeginning; + 1 : Origin := soFromCurrent; + 2 : Origin := soFromEnd; + else + Origin := soFromBeginning; + end; + Result := Stream.Seek(offset,origin); +end; + +const + AVFileBufSize = 32768; + +type + PAVFile = ^TAVFile; + TAVFile = record + ByteIOCtx: TByteIOContext; + Buffer: array [0 .. AVFileBufSize-1] of byte; + end; + +function AVOpenInputFile(var ic_ptr: PAVFormatContext; filename: IPath; + fmt: PAVInputFormat; buf_size: cint; + ap: PAVFormatParameters): PAVFile; +var + ProbeData: TAVProbeData; + FilenameStr: UTF8String; + FileStream: TBinaryFileStream; + AVFile: PAVFile; +begin + Result := nil; + New(AVFile); + + // copy UTF-8 string so ProbeData.filename will be valid at av_probe_input_format() + FilenameStr := Filename.ToUTF8; + + if (fmt = nil) then + begin + ProbeData.filename := PChar(FilenameStr); + ProbeData.buf := @AVFile.Buffer; + ProbeData.buf_size := AVFileBufSize; + + fmt := av_probe_input_format(@ProbeData, 1); + if (fmt = nil) then + begin + Dispose(AVFile); + Exit; + end; + end; + + FileStream := TBinaryFileStream.Create(Filename, fmOpenReadWrite); + if (init_put_byte(@AVFile.ByteIOCtx, @AVFile.Buffer, AVFileBufSize, 0, FileStream, + FFmpegStreamRead, FFmpegStreamWrite, FFmpegStreamSeek) < 0) then + begin + Dispose(AVFile); + Exit; + end; + + if (av_open_input_stream(ic_ptr, @AVFile.ByteIOCtx, PChar(FilenameStr), fmt, ap) < 0) then + begin + Dispose(AVFile); + Exit; + end; + + Result := AVFile; +end; + + { TPacketQueue } constructor TPacketQueue.Create(); |