Skip to content

Commit

Permalink
Merge pull request #947 from TheNailDev/version-handling
Browse files Browse the repository at this point in the history
Implement UltraStar File Format 1.0.0
  • Loading branch information
basisbit authored Jan 21, 2025
2 parents 3b13eb3 + a939c1b commit 0d19a33
Show file tree
Hide file tree
Showing 2 changed files with 234 additions and 32 deletions.
23 changes: 14 additions & 9 deletions src/base/UFiles.pas
Original file line number Diff line number Diff line change
Expand Up @@ -126,15 +126,20 @@ function SaveSong(const Song: TSong; const Tracks: array of TLines; const Name:
try
SongFile := TMemTextFileStream.Create(Name, fmCreate);
try
// to-do: should we really write the BOM?
// it causes problems w/ older versions
// e.g. usdx 1.0.1a or ultrastar < 0.7.0
if (Song.Encoding = encUTF8) then
SongFile.WriteString(UTF8_BOM);

// do not save "auto" encoding tag
if (Song.Encoding <> encAuto) then
SongFile.WriteLine('#ENCODING:' + EncodingName(Song.Encoding));
if Song.FormatVersion.MinVersion(1,0,0,true) then
begin
// Only save version if it is at least 1.0.0
SongFile.WriteLine('#VERSION:' + EncodeToken(Song.FormatVersion.VersionString));
// RELATIVE was removed in format 1.0.0
Relative := False;
end
else
begin
// Only save Encoding if Version is below 1.0.0
// do not save "auto" encoding tag
if (Song.Encoding <> encAuto) then
SongFile.WriteLine('#ENCODING:' + EncodingName(Song.Encoding));
end;
SongFile.WriteLine('#TITLE:' + EncodeToken(Song.Title));
SongFile.WriteLine('#ARTIST:' + EncodeToken(Song.Artist));

Expand Down
243 changes: 220 additions & 23 deletions src/base/USong.pas
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,17 @@ TCustomHeaderTag = record
Content: UTF8String;
end;

TVersion = class
private
Major, Minor, Patch: integer;
public
constructor Create(); overload;
constructor Create(const VersionString: string); overload;
function MinVersion(Major, Minor, Patch: integer; Inclusive: boolean = true): boolean;
function MaxVersion(Major, Minor, Patch: integer; Inclusive: boolean = false): boolean;
function VersionString: string;
end;

TSong = class
private
FileLineNo : integer; // line, which is read last, for error reporting
Expand All @@ -126,6 +137,8 @@ TSong = class
FileName: IPath; // just name component of file (only set if file was found)
MD5: string; //MD5 Hash of Current Song

FormatVersion: TVersion;

// filenames
Cover: IPath;
Mp3: IPath;
Expand Down Expand Up @@ -197,6 +210,7 @@ TSong = class

constructor Create(); overload;
constructor Create(const aFileName : IPath); overload;
destructor Destroy; override;
function LoadSong(DuetChange: boolean): boolean;
function Analyse(const ReadCustomTags: Boolean = false; DuetChange: boolean = false; RapToFreestyle: boolean = false): boolean;
procedure SetMedleyMode();
Expand Down Expand Up @@ -256,6 +270,105 @@ constructor TSongOptions.Create(RatioAspect, Width, Height, Position, Alpha: int

end;

constructor TVersion.Create();
begin
inherited;

Self.Major := 0;
Self.Minor := 3;
Self.Patch := 0;
end;

constructor TVersion.Create(const VersionString: string);
var
SepPos: integer;
SubVersion: string;
begin
inherited Create();

SepPos := Pos('.', VersionString);
// If Version does not contain periods it is invalid
if (SepPos = 0) then
raise Exception.Create('Invalid VERSION "' + VersionString +'"');
// Read the major version as section in front of (first) period
try
Self.Major := StrToInt(Trim(Copy(VersionString, 1, SepPos - 1)));
except
on E : EConvertError do
raise Exception.Create('Invalid VERSION Header "' + VersionString + '"');
end;
// The minor and patch version number "x.x" is the SubVersion
SubVersion := Trim(Copy(VersionString, SepPos + 1, Length(VersionString) - SepPos));
SepPos := Pos('.', SubVersion);
// The Version must contain a second period or otherwise it is invalid
if (SepPos = 0) then
begin
raise Exception.Create('Invalid VERSION "' + VersionString +'"');
end;
// Read the minor version as section in between first and second period
// and the patch version as section after the second period
try
Self.Minor := StrToInt(Trim(Copy(SubVersion, 1, SepPos - 1)));
Self.Patch := StrToInt(Trim(Copy(SubVersion, SepPos + 1, Length(VersionString) - SepPos)));
except
on E : EConvertError do
raise Exception.Create('Invalid VERSION "' + VersionString +'"');
end;
end;

function TVersion.MinVersion(Major, Minor, Patch: integer; Inclusive: boolean = true): boolean;
begin
if (Self.Major > Major) then
Result := true
else if (Self.Major = Major) then
begin
if (Self.Minor > Minor) then
Result := true
else if (Self.Minor = Minor) then
begin
if (Self.Patch > Patch) then
Result := true
else if (Inclusive and (Self.Patch = Patch)) then
Result := true
else
Result := false;
end
else
Result := false;
end
else
Result := false;
end;

function TVersion.MaxVersion(Major, Minor, Patch: integer; Inclusive: boolean = false): boolean;
begin
if (Self.Major < Major) then
Result := true
else if (Self.Major = Major) then
begin
if (Self.Minor < Minor) then
Result := true
else if (Self.Minor = Minor) then
begin
if (Self.Patch < Patch) then
Result := true
else if (Inclusive and (Self.Patch = Patch)) then
Result := true
else
Result := false;
end
else
Result := false;
end
else
Result := false;
end;

function TVersion.VersionString: string;
begin
Result := IntToStr(Self.Major) + '.' + IntToStr(Self.Minor) + '.' + IntToStr(Self.Patch);
end;

constructor TSong.Create();
begin
inherited;
Expand Down Expand Up @@ -337,6 +450,12 @@ constructor TSong.Create(const aFileName: IPath);
*)
end;

destructor TSong.Destroy;
begin
FreeAndNil(Self.FormatVersion);
inherited;
end;

function TSong.FindSongFile(Dir: IPath; Mask: UTF8String): IPath;
var
Iter: IFileIterator;
Expand Down Expand Up @@ -775,7 +894,7 @@ function TSong.ReadTXTHeader(SongFile: TTextFileStream; ReadCustomTags: Boolean)
{ Removes all entries for a given header-tag from the TagMap
If TagMap contains multiple entries for the given tag,
a informative message about the duplicate tags is logged }
procedure RemoveTagsFromTagMap(const tag: string);
procedure RemoveTagsFromTagMap(const tag: string; logDuplicateMsg: boolean = true);
var
count: Integer;
begin
Expand All @@ -785,7 +904,7 @@ function TSong.ReadTXTHeader(SongFile: TTextFileStream; ReadCustomTags: Boolean)
TagMap.Remove(tag);
count := count + 1;
end;
if count > 1 then
if logDuplicateMsg and (count > 1) then
begin
Log.LogInfo('Duplicate Tag "'+ tag +'" found in file ' + FullFileName + '. Only the last value will be used.',
'TSong.ReadTXTHeader.RemoveTagsFromTagMap');
Expand Down Expand Up @@ -876,6 +995,50 @@ function TSong.ReadTXTHeader(SongFile: TTextFileStream; ReadCustomTags: Boolean)

//Read the songs attributes stored in the TagMap

//First: Read the format version
if (TagMap.TryGetData('VERSION', Value)) then
begin
RemoveTagsFromTagMap('VERSION');
try
self.FormatVersion := TVersion.Create(Value);
except
on E: Exception do
begin
Result := false;
Log.LogError(E.Message + ' in Song File: ' + FullFileName);
Exit;
end
end;
if not self.FormatVersion.MaxVersion(2,0,0,false) then
begin
Result := false;
Log.LogError('Unsupported format version ' + self.FormatVersion.VersionString + '; Maximum supported version is 1.X.X: ' + FullFileName);
Exit;
end;
end
else
self.FormatVersion := TVersion.Create; //Default legacy version 0.3.0

// For Version >=1.0.0 Encoding is always UTF-8
// For Version <1.0.0 read Encoding from ENCODING header
if Self.FormatVersion.MinVersion(1,0,0,true) then
begin
self.Encoding := encUTF8;
if TagMap.IndexOf('ENCODING') > -1 then
begin
Log.LogInfo('Ignoring ENCODING header in file "' + FullFileName + '" (deprecated in Format 1.0.0)', 'TSong.ReadTXTHeader');
RemoveTagsFromTagMap('ENCODING', false);
end;
end
else
begin
if TagMap.TryGetData('ENCODING', Value) then
begin
RemoveTagsFromTagMap('ENCODING');
self.Encoding := ParseEncoding(Value, Ini.DefaultEncoding);
end;
end;

//-----------
//Required Attributes
//-----------
Expand Down Expand Up @@ -1032,35 +1195,53 @@ function TSong.ReadTXTHeader(SongFile: TTextFileStream; ReadCustomTags: Boolean)
// Resolution
if (TagMap.TryGetData('RESOLUTION', Value)) then
begin
RemoveTagsFromTagMap('RESOLUTION');
TryStrtoInt(Value, self.Resolution);
if (self.Resolution < 1) then
if FormatVersion.MaxVersion(1,0,0,false) then
begin
RemoveTagsFromTagMap('RESOLUTION');
TryStrtoInt(Value, self.Resolution);
if (self.Resolution < 1) then
begin
Log.LogError('Ignoring invalid resolution in song: ' + FullFileName);
self.Resolution := DEFAULT_RESOLUTION;
end;
end
else
begin
Log.LogError('Ignoring invalid resolution in song: ' + FullFileName);
self.Resolution := DEFAULT_RESOLUTION;
Log.LogInfo('Ignoring RESOLUTION header in file "' + FullFileName + '" (deprecated in Format 1.0.0)', 'TSong.ReadTXTHeader');
RemoveTagsFromTagMap('RESOLUTION', false);
end;
end;

// Notes Gap
if (TagMap.TryGetData('NOTESGAP', Value)) then
begin
RemoveTagsFromTagMap('NOTESGAP');
TryStrtoInt(Value, self.NotesGAP)
if FormatVersion.MaxVersion(1,0,0,false) then
begin
RemoveTagsFromTagMap('NOTESGAP');
TryStrtoInt(Value, self.NotesGAP)
end
else
begin
Log.LogInfo('Ignoring NOTESGAP header in file "' + FullFileName + '" (deprecated in Format 1.0.0)', 'TSong.ReadTXTHeader');
RemoveTagsFromTagMap('NOTESGAP', false);
end;
end;

// Relative Notes
if (TagMap.TryGetData('RELATIVE', Value)) then
begin
RemoveTagsFromTagMap('RELATIVE');
if (UpperCase(Value) = 'YES') then
self.Relative := true;
end;

// File encoding
if (TagMap.TryGetData('ENCODING', Value)) then
begin
RemoveTagsFromTagMap('ENCODING');
self.Encoding := ParseEncoding(Value, Ini.DefaultEncoding);
if FormatVersion.MaxVersion(1,0,0,false) then
begin
RemoveTagsFromTagMap('RELATIVE');
if (UpperCase(Value) = 'YES') then
self.Relative := true;
end
else
begin
Result := false;
Log.LogError('Relative Mode was removed for format >=1.0.0. The song will not be loaded. ' + FullFileName);
Exit;
end;
end;

// PreviewStart
Expand Down Expand Up @@ -1102,15 +1283,31 @@ function TSong.ReadTXTHeader(SongFile: TTextFileStream; ReadCustomTags: Boolean)
// Duet Singer Name P1
if (TagMap.TryGetData('DUETSINGERP1', Value)) then
begin
RemoveTagsFromTagMap('DUETSINGERP1');
DecodeStringUTF8(Value, DuetNames[0], Encoding);
if FormatVersion.MaxVersion(1,0,0,false) then
begin
RemoveTagsFromTagMap('DUETSINGERP1');
DecodeStringUTF8(Value, DuetNames[0], Encoding);
end
else
begin
Log.LogInfo('Ignoring DUETSINGERP1 header in file "' + FullFileName + '" (deprecated in Format 1.0.0)', 'TSong.ReadTXTHeader');
RemoveTagsFromTagMap('DUETSINGERP1', false);
end;
end;

// Duet Singer Name P2
if (TagMap.TryGetData('DUETSINGERP2', Value)) then
begin
RemoveTagsFromTagMap('DUETSINGERP2');
DecodeStringUTF8(Value, DuetNames[1], Encoding);
if FormatVersion.MaxVersion(1,0,0,false) then
begin
RemoveTagsFromTagMap('DUETSINGERP2');
DecodeStringUTF8(Value, DuetNames[1], Encoding);
end
else
begin
Log.LogInfo('Ignoring DUETSINGERP2 header in file "' + FullFileName + '" (deprecated in Format 1.0.0)', 'TSong.ReadTXTHeader');
RemoveTagsFromTagMap('DUETSINGERP2', false);
end;
end;

// Duet Singer Name P1
Expand Down

0 comments on commit 0d19a33

Please sign in to comment.