Tags Library

Started by 3delite,

Steve Grant

Thanks for that I will give it a try, however for the moment I have gone back to Bass getting bitrate etc.

I have a load of mp3's that Bass/dbPoweramp/mp3tag all return the correct bitrate. Tagslib returns 0. (They all play fine).

Here is one for you to play with https://www.dropbox.com/s/3g09fur83dtruav/01%20-%20Marina%20%26%20The%20Diamonds%20-%20Radioactive.mp3?dl=1

Steve Grant

Just for your info I am turning on the relevant config thus;

       TagsLibrary_SetConfig lTags, 1, 2, ttAutomatic
    TagsLibrary_SetConfig lTags, 1, 3, ttAutomatic
    TagsLibrary_SetConfig lTags, 1, 5, ttAutomatic
    TagsLibrary_SetConfig lTags, 1, 6, ttAutomatic
    TagsLibrary_SetConfig lTags, 1, 7, ttAutomatic

It makes no difference.

Steve Grant

Hi again,

I've come across a file that TagsLib simply will not write to. Even the old AudioGenie that I used to use before TagsLib will write to it.

I've checked the file properties also the security and all is ok.

https://www.dropbox.com/s/x6iecjjugu4r4wa/CD1423Track76.mp3?dl=1

3delite

Uploaded an update that fixes that 2 files, please download it, link in the first post.

The config option should be like:

TagsLibrary_SetConfig lTags, 1, TAGSLIBRARY_DEEP_OPUS_BITRATE_SCAN, ttAutomatic

Please check if the function returns a non-zero value, 0 means failure.

But looking at the code, sorry, it only works explicitly with Opus files (probably that's why the name contains 'Opus', it was a long ago that I coded this), and for Vorbis files only used when the 'BitRateNominal' is 0 by the Vorbis header.

Steve Grant

#454
That works great thank you very much.

Quote from: 3deliteThe config option should be like:

TagsLibrary_SetConfig lTags, 1, TAGSLIBRARY_DEEP_OPUS_BITRATE_SCAN, ttAutomatic

Please check if the function returns a non-zero value, 0 means failure.

Yes the function returns non-zero, after all
Public Const TAGSLIBRARY_DEEP_OPUS_BITRATE_SCAN As Long = 7means I can use TAGSLIBRARY_DEEP_OPUS_BITRATE_SCAN or 7.

Whilst we are having so much luck here are two more, they both return 128 but 1) should be 144 and 2) should be 260!

files removed 08/02/2018

3delite

Yes, TAGSLIBRARY_DEEP_OPUS_BITRATE_SCAN = 7, but it's always a better idea to use the const names.

MP3 (MPEG) files are only processed by the first MPEG frame. So the bit rate is only valid for CBR MPEG files. Please use BASS_ChannelGetAttribute() with the BASS_ATTRIB_BITRATE option for getting the bit rate. You probably want to get the playtime anyway by BASS already so no need to scan the file twice.

EWeiss

#456
Hi 3delite

one question can you write Tags for mp3 in this Format?
https://msdn.microsoft.com/en-us/library/windows/desktop/dd743220(v=vs.85).aspx

so it's compatible to WMP to ?
i can not see any cover in WIndowsMediaplayer_10 Plugin while the original Tag of mp3 is not supported.

i am missing private Frames in the Tag like this!
without the cover not show..

see Picture WMP.png

also what i am missing.. is write this to the Tag! (mp3Files)

PRIV WM/MediaClassSecondaryID
PRIV WM/MediaClassPrimaryID
PRIV WM/WMContentID
PRIV WM/WMCollectionID
PRIV WM/WMCollectionGroupID
PRIV WM/UniqueFileIdentifier

greets

3delite

ID3v2.2 format tag is only supported for loading, they are converted to according ID3v2.3/4 frames. When saving back these tags they are written in ID3v2.3 format.
id3.org says ID3v2.2 format is obsolete and should not be used.
Check "function TID3v2Tag.Convertv2Tov3()" in ID3v2Library.pas for the conversion details.

EWeiss

#458
Quote from: 3deliteID3v2.2 format tag is only supported for loading, they are converted to according ID3v2.3/4 frames. When saving back these tags they are written in ID3v2.3 format.
id3.org says ID3v2.2 format is obsolete and should not be used.
Check "function TID3v2Tag.Convertv2Tov3()" in ID3v2Library.pas for the conversion details.

i hope your understand what i want?
i want add the
PRIV WM/MediaClassSecondaryID Frames to my mp3 Files so Bliss\Windowsmediaplayer_10.dll Visualization read my cover correctly.
without use WMP for Tagging.

EDIT:
It's finish Forget it also.. thanks
i have create it self with help of other People to

that is what i want.. see shot.
Show AlbumArt without taggin over WMP.

greets

3delite

#459
Sorry, not clear to me.

You want to add an ID3v2 PRIV frame that contains a GUID?

If this is the case, you can set it with something like:

procedure TForm1.Button3Click(Sender: TObject);
var
    Error: Integer;
    GUID: TGUID;
    GUIDString: ANSIString;
    FrameIndex: Integer;
    TagData: TTagData;
begin
    CreateGUID(GUID);
    GUIDString := GUIDToString(GUID);

    FrameIndex := TagsLibrary_AddTag(Tags, PChar('PRIV'), PChar(''), ttID3v2); //* Add an empty PRIV frame

    TagData.Name := PChar('PRIV');
    TagData.Data := PChar(GUIDString); //* Pointer to first byte of the data
    TagData.DataSize := Length(GUIDString); //* Size of this data
    TagData.DataType := 0; //* Not used for ID3v2

    TagsLibrary_SetTagData(Tags, FrameIndex, ttID3v2, TagData);

    Error := TagsLibrary_Save(Tags, PWideChar(Edit1.Text), ttID3v2);
end;

It's possible to create any frame with any data this way if this is what you wish.

EDIT: So a PRIV frame should be like this correctly:

procedure TForm1.Button3Click(Sender: TObject);
var
    Error: Integer;
    GUID: TGUID;
    GUIDString: ANSIString;
    FrameIndex: Integer;
    TagData: TTagData;
begin
    CreateGUID(GUID);
    GUIDString := 'WM/MediaClassSecondaryID' + #0 + GUIDToString(GUID);

    FrameIndex := TagsLibrary_AddTag(Tags, PChar('PRIV'), PChar(''), ttID3v2); //* Add an empty PRIV frame

    TagData.Name := PChar('PRIV');
    TagData.Data := PChar(GUIDString); //* Pointer to first byte of the data
    TagData.DataSize := Length(GUIDString); //* Size of this data
    TagData.DataType := 0; //* Not used for ID3v2

    TagsLibrary_SetTagData(Tags, FrameIndex, ttID3v2, TagData);

    Error := TagsLibrary_Save(Tags, PWideChar(Edit1.Text), ttID3v2);
end;

EWeiss

yes that is what i want but i have do it self

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, mp3FileUtils, StdCtrls, ShlObj, ActiveX;

const
  cMP3Error : Array[TMP3Error] of String = (
    'MP3ERR_None',
    'MP3ERR_NoFile',
    'MP3ERR_FOpenCrt',
    'MP3ERR_FOpenR',
    'MP3ERR_FOpenRW',
    'MP3ERR_FOpenW',
    'MP3ERR_SRead',
    'MP3ERR_SWrite',
    'ID3ERR_Cache',
    'ID3ERR_NoTag',
    'ID3ERR_Invalid_Header',
    'ID3ERR_Compression',
    'ID3ERR_Unclassified',
    'MPEGERR_NoFrame'
   );

type
  TForm1 = class(TForm)
    btnSearch: TButton;
    edSearchpath: TEdit;
    Label1: TLabel;
    Label2: TLabel;
    lblProgress: TLabel;
    btnDoIt: TButton;
    Label3: TLabel;
    lblCount: TLabel;
    btnHelp: TButton;
    lstSearch: TListBox;
    lblMessage: TLabel;
    lblMessageVal: TLabel;
    procedure btnSearchClick(Sender: TObject);
    procedure btnHelpClick(Sender: TObject);
    procedure btnDoItClick(Sender: TObject);
    procedure lstSearchDrawItem(Control: TWinControl; Index: Integer; Rect: TRect;
      State: TOwnerDrawState);
  private
    { Private-Deklarationen }
    failed: array of BOOL;
    ProgressCounter: Integer;
    procedure WinProcessMessages;
    procedure SetCoverpic(Picfile: string; mp3file: string; Index: Integer);
    function GetFolder(const ARoot: integer; const ACaption: String): String;
    procedure FindMediaFiles(const FileList: tstrings; RootFolder: string;
      Maske: array of string; Recurse: Boolean);
  public
    { Public-Deklarationen }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.btnDoItClick(Sender: TObject);
var
  IntI: Integer;
  strCover, oldCover: string;
begin

  oldCover := '';
  ProgressCounter:= lstSearch.Count;

  for IntI := 0 to (lstSearch.Count - 1) do
  begin
    failed[IntI] := false;
    if oldCover <> ExtractFilePath(lstSearch.Items[IntI]) then
    begin
      strCover := ExtractFilePath(lstSearch.Items[IntI]);
      strCover := strCover + lowercase('AlbumArtSmall.jpg');
      oldCover := ExtractFilePath(lstSearch.Items[IntI]);
    end;

    if FileExists(strCover) then
    begin
      SetCoverpic(strCover, lstSearch.Items[IntI], IntI);
      dec(ProgressCounter);
      lblProgress.Caption := IntToStr(ProgressCounter);
      lblProgress.Repaint;
      lstSearch.Selected[IntI] := true;
    end else
    begin
      failed[IntI] := true;
      dec(ProgressCounter);
      lblProgress.Caption := IntToStr(ProgressCounter);
      lblProgress.Repaint;
      lstSearch.Repaint;
      lstSearch.Selected[IntI] := true;
    end;
  end;
end;

procedure TForm1.btnHelpClick(Sender: TObject);
var
  pString: string;
begin

  pString := 'the application writes PRIV: Frames to MP3 Tag' + #13#10 +
    'necessary to make the visual plugins for WMP work properly'  + #13#10 +
    'without having created the Mp3 tag via WMP (Windows Media Player)'  + #13#10 +
    'a file called AlbumArtSmall.jpg is required in the Search path' + #13#10 +
    #13#10 +
    'WARNING: after executing the function over DoIt, process can''t be stopped' + #13#10 +
    #13#10 +
    'Artist' + #13#10 +
    'Album' + #13#10 +
    'Title' + #13#10 +
    'Year' + #13#10 +
    'Track' + #13#10 +
    'Genre' + #13#10 +
    'Comment' + #13#10 +
     #13#10 +
    'are not changed' + #13#10 +
    'if your use more entries in the Mp3 tag' + #13#10 +
    'please close the application, or your data will be lost!';
  MessageBox(Handle, PWideChar(pString), 'Readme first', MB_OK);
end;

procedure TForm1.btnSearchClick(Sender: TObject);
var
  List: TStringList;
  IntI: Integer;
  Mask: array[0..0] of string;
begin
  edSearchpath.Text := GetFolder(CSIDL_DRIVES, 'Select search path');

  If edSearchpath.Text <> '' then
  begin
     if lstSearch.Count > 0 then
       lstSearch.clear;

     List := TStringList.Create;
     try
       Mask[0] := '.mp3';

       FindMediaFiles(List, edSearchpath.Text, Mask, True);
       lblCount.Caption := IntToStr(List.Count);
       ProgressCounter := List.Count;
       lblProgress.Caption := IntToStr(ProgressCounter);

       if List.Count > 0 then
       begin
         btnDoIt.Enabled := true;
         Setlength(failed, List.Count);
       end;

       for IntI := 0 to List.Count - 1 do
         lstSearch.Items.Add(List[IntI]);
     finally
       List.Free;
     end;
  end;
end;

procedure TForm1.FindMediaFiles(const FileList: tstrings; RootFolder: string;
  Maske: array of string; Recurse: Boolean);
var
  SR: TSearchRec;
  i: Integer;
begin

  RootFolder := IncludeTrailingPathDelimiter(RootFolder);

  if Recurse then
    if FindFirst(RootFolder + '*.*', faAnyFile, SR) = 0 then
      try
        repeat
          if SR.Attr and faDirectory = faDirectory then
            if (SR.Name <> '.') and (SR.Name <> '..') then
              FindMediaFiles(FileList, RootFolder + SR.Name, Maske, Recurse);
        until FindNext(SR) <> 0;
      finally
        FindClose(SR);
      end;
  i := 0;
  repeat
  begin
    if FindFirst(RootFolder + '*' + Maske[i], faAnyFile, SR) = 0 then
      try
        repeat
          if SR.Attr and faDirectory <> faDirectory then
          begin
            FileList.add(RootFolder + SR.Name);
          end;
        until FindNext(SR) <> 0;
      finally
        FindClose(SR);
      end;
    i := i + 1;
  end
  until i = high(Maske) + 1;
end;

function TForm1.GetFolder(const ARoot: integer; const ACaption: String): String;
var
  bi: TBROWSEINFO;
  lpBuffer: PChar;
  pidlPrograms,
  pidlBrowse: PItemIDList;
  ShellH: IMalloc;
begin
  pidlBrowse := nil;

  if (not SUCCEEDED(SHGetSpecialFolderLocation(GetActiveWindow,
                                               ARoot,
                                               pidlPrograms))) then
    Exit;
  try
    GetMem(lpBuffer, MAX_PATH);
    try
      bi.hwndOwner:=GetActiveWindow;
      bi.pidlRoot:=pidlPrograms;
      bi.pszDisplayName:=lpBuffer;
      bi.lpszTitle:=PChar(ACaption);
      bi.ulFlags:=BIF_RETURNONLYFSDIRS;
      bi.lpfn:=NIL;
      bi.lParam:=0;
      pidlBrowse:=SHBrowseForFolder(bi);

      if (pidlBrowse <> nil) and (SHGetPathFromIDList(pidlBrowse,
                                                      lpBuffer)) then
        Result:=lpBuffer;
    finally
      FreeMem(lpBuffer);
    end;
  finally
    if SHGetMalloc(ShellH) = NOERROR then
       ShellH.Free(pidlBrowse);
  end;
end;

procedure TForm1.lstSearchDrawItem(Control: TWinControl; Index: Integer; Rect: TRect;
  State: TOwnerDrawState);
begin
  with (Control as TListBox).Canvas do
  begin
    if not (odFocused in State) then
    begin
      if failed[Index] then
      begin
        Brush.Color:= clWebDarkOrange;
        Font.Color:= clBlack;
      end else
      begin
        Brush.Color := clWebLightGreen;
        Font.Color:= clBlack;
      end;
      FillRect(Rect);
      TextOut(Rect.Left + 2, Rect.Top, (Control as TListBox).Items[Index]);
    end;
  end;
end;

procedure TForm1.SetCoverpic(Picfile, mp3file: string; Index: Integer);
var
   MP3Tags, v23Tag: TId3v2Tag;
   PicData: TMemorystream;
   GUID: TMemorystream;
   error : TMP3Error;
   i: Integer;
   b: Byte;
begin
    MP3Tags:= TId3v2Tag.Create;
    v23Tag := TId3v2Tag.Create;
    PicData:= TMemoryStream.Create;
    GUID := TMemorystream.Create;

    try
      Error := MP3Tags.ReadFromFile(mp3File);
      If Error = MP3ERR_None then
      begin
        Picdata.LoadFromFile(Picfile);

        // basic converting
        v23Tag.Artist := MP3Tags.Artist;
        v23Tag.Album := MP3Tags.Album;
        v23Tag.Title := MP3Tags.Title;
        v23Tag.Year  := MP3Tags.Year;
        v23Tag.Track := MP3Tags.Track;
        v23Tag.Genre := MP3Tags.Genre;
        v23Tag.Comment := MP3Tags.Comment;

        if MP3Tags.Rating = 0 then
          v23Tag.Rating := 128 // 3 Sterne
        else
        v23Tag.Rating := MP3Tags.Rating;

        for i := 1 to 16 do
        begin
           b := Random(255);
           GUID.Write(b, 1);
        end;

        v23Tag.SetPrivateFrame('WM/WMCollectionID', GUID);
        v23Tag.SetPrivateFrame('WM/WMCollectionGroupID', GUID);

        v23Tag.SetPicture('image/jpeg', 0, '*', PicData);
        v23Tag.WriteToFile(mp3file);

      end else
        lblMessageVal.Caption := cMP3Error[Error];
    finally
      FreeAndNIL(MP3Tags);
      FreeAndNIl(PicData);
      FreeAndNIl(GUID);

      FreeAndNIl(v23Tag);
      Winprocessmessages;
    end;
end;

procedure TForm1.WinProcessMessages;
// Allow Windows to process other system messages
var
  ProcMsg: TMsg;
begin
  while PeekMessage(ProcMsg, 0, 0, 0, PM_REMOVE) do
  begin
    if (ProcMsg.message = WM_QUIT) then
      Exit;
    TranslateMessage(ProcMsg);
    DispatchMessage(ProcMsg);
  end;
end;

end.

thanks for your time..

greets


Steve Grant

Hi 3delite I have found another file that Tags Lib will not write to.

AudioGenie, dBpoweramp etc are all ok.

https://www.dropbox.com/s/7j0f0dhjrv5ocin/CD3081Track91.flac?dl=1

3delite

Quick test here seemed working fine.

Please check if you are using the latest version: https://www.3delite.hu/Object%20Pascal%20Developer%20Resources/download.html#tagslibrary

Also check if the file there is not read-only, btw. what error code does Tags Library give when saving the tags?

Steve Grant

Thank you for testing, I have no-one else I can ask for this. Inside my Save Tags function I call the LCMapStringW api to perform TitleCase for most of the fields. (Artist - Title etc).

Because you said Tags Lib was ok at your end, I tested further and found that the above api can be tripped up. I have stopped using this for a more manual method and all now seems well.

I check the main download page at least every 2 weeks to ensure I have the latest version.

Many thanks for your help and I am sorry for wasting your time. I have removed the track.

Steve.

Pauven

Hi 3delite,

I am using the Tags Library in a new music library program.  Before a user plays any music, they assign a folder and my Delphi program scans the folder to find all the music, then it reads the tags, building a local database of all their music.

I am getting very weird results with the PlayTime attribute.  Sometimes the value appears to be in seconds (i.e. 203.xxx seconds, or 3:23), while other times it is a much larger value like 2030000.  When I see the larger value, I divide by 10000 and get the right value.  While it would be nice if the PlayTime was always seconds and not seconds x 10000, I can handle this.

But there's also a 3rd scenario happening that I don't understand. I recently started testing FLAC files, and find that I am getting random results.  Sometimes the playtime is correct, and sometimes it seems almost like a random number.  But the random number is always the same value every time I read it, so it's not random but it is wrong.

Here's some examples from the Beatles Love album that I have ripped 3 different ways to FLAC:

The Beatles - Help! (Love Remix)
CD Audio Rip to FLAC 44.1KHz 16Bit = 138.56        <<<Correct
DVD Audio Rip to FLAC 96KHz 16Bit  = 138.6          <<<Correct
DVD Audio Rip to FLAC 96KHz 24Bit  = 36254.4455 <<<WRONG, 261.58 x's too high

The Beatles - Because (Love Remix)
CD Audio Rip to FLAC 44.1KHz 16Bit = 1592.68675       <<<WRONG, 9.73 x's too high
DVD Audio Rip to FLAC 96KHz 16Bit  = 163.62666667   <<<Correct
DVD Audio Rip to FLAC 96KHz 24Bit  = 163.62666667   <<<Correct

I'm not sure if this is a problem with FLAC only, but so far that is the only format I've noticed having this issue.  When I view these same files using Windows 10 file properties or Mp3tag, the correct length is always reported.

Is this something you can help with?

Thanks,
Paul

Pauven

Also, possibly related to my issue in the previous post, I noticed this in the TSourceAudioAttributes.GetPlayTime function:

    for i := High(Parent.TagLoadPriority) downto Low(Parent.TagLoadPriority) do begin
        if Parent.TagLoadPriority = TagsLibrary.ttFlac then begin
            if Parent.FlacTag.Channels <> 0 then begin
                Result := Parent.FlacTag.PlayTime;
                Exit;
            end;
        end;
        if Parent.TagLoadPriority = TagsLibrary.ttID3v2 then begin
            if Parent.ID3v2Tag.SourceFileType = sftMPEG then begin
                if Parent.ID3v2Tag.PlayTime <> 0 then begin
                    Result := Parent.ID3v2Tag.PlayTime;
                    Exit;
                end;
            end;


For FLAC, it is only getting the playtime if the Channels <> 0, but for all other tag types, it checks if PlayTime <> 0 (or Duration on WMA).  Maybe this is correct, but it seems like it might be a typo.

3delite

#467
I check the playtime, might be that it's sometimes seconds sometimes milliseconds... sorry...

But for playtime a much more reliable way is to use BASS for getting the value, it takes more time though. Tags Library is for tags, I did add some audio attributes support but it's a tagging library.

Thank You very much for reporting these though!

PS. Can You please tell me what file formats You get a non-seconds (large) values?

EDIT: It seems like the problem is that the Flac file is falsely identified as a MPEG file, and the MPEG playtime return was before Flac. Attached an update, that now first tests if the file is Flac first.

Pauven

Thanks 3delite!  I'll give the new version a test.

I was only getting the bad length values with FLAC files, but I also only have MP3, WMA, WAV and FLAC files to test with.  I believe one of my beta testers has also tested AAC and hasn't reported any issues.  My program is setup to run with almost every audio format, but I've yet to test other formats.

How would I effectively use BASS to read the length for more than 40,000 files without playing them?  My collection has over 42,000 songs, and I'm sure some of my users will have even larger collections.  It is currently taking about 20 minutes to scan my collection and read the tags.  I don't know how to do this will BASS without loading each file for playing, and I'm worried that it might make the scan time much slower, perhaps hours.  I really like the simplicity of doing this with the Tags Library, which is why I purchased a license, so if we can get the occasional issue like this worked out, I'll be a very happy camper.  I don't need the song length to be super accurate, close is good enough for me.

Thanks,
Paul

3delite

#469
Tags Library has been updated a couple of days ago, please download the latest package, then replace the 2 files that I attached.

Tags Library download

That 20 minutes sounds quite a bit, well please try using Tags Library and report if playtime parsing fails on some files.
Using BASS for getting the playtime will probably increase this time quite a bit. If You decide to use BASS, something like:

function GetBASSPlayTime(FileName: String): Double;
var
    Channel: Cardinal;
begin
    Channel := BASS_StreamCreateFile(False, PChar(FileName), 0, 0, BASS_UNICODE OR BASS_STREAM_DECODE {OR BASS_STREAM_PRESCAN});
    Result := BASS_ChannelBytes2Seconds(Channel, BASS_ChannelGetLength(Channel));
    if Result < 0 then begin
        Result := 0;
    end;
    BASS_StreamFree(Channel);
end;

If You don't need cover arts when scanning, use 'Tags.ParseCoverArts := False;' it might speed things up a bit.

EDIT: And one more: 'Tags.ID3v2Tag.MPEGSearchLength := 0;' this will not check for corrupt MP3 files. Can also help a bit with speed.

3delite

Did a quick test, with SSD, 6524 files (mainly MP3, some MP4 & Flac).

First try: 15 seconds.
Second try (Windows already cached the files): 9 seconds.
Setting that 2 options that I mentioned: 7.5 seconds.

Here's the test app.: Tag Parse Speed Tester.zip

You can check this and maybe optimize the scanner code. Hope it helps!

Pauven

Wow, awesome, I wasn't expecting help with the scanning speed.  Thanks so much!

Paul

Pauven

The FLAC song length issue is fixed, thank you thank you thank you!!!  About half the times are identical to what BASS reports, and the other half show 1 second longer than BASS, so it is very accurate.

I can't seem to use the Tags.ParseCoverArts, is that an old function that you have either removed or renamed?  It doesn't exist in the TagsLibrary.pas file.

I hadn't timed my sync recently, so I just timed a before and after with the MPEGSearchLength setting.  I was wrong to say 20 minutes (that might have been for one of my earlier test versions) as it is much faster.  For both tests, I had already done a prior scan so it was with cached files (all local on SSD).

Before:  2:58

After:  2:48

So I did get a slight improvement, but the difference is small enough that it might have just been normal variation.

I then ran your speed tester, and got:  Took: 00:02:12.94 - Files: 68363

So your speed tester is faster, though I'll have to look at what it is doing internally, as it might not be doing as much work as my scan is doing, as I'm processing tag data, building a database, and logging issues.

Pauven

I just ran another test, and got my scanning time down to 1:58, even faster than your speed parsing test.  Considering I'm building a full database during the scan, including tables of all artists and genres, among other things, I think that seems okay.

The change I made is how often I refresh the screen with the current progress of the scan.  With the updates every 11 files, I got 2:48, but reducing that to every 101 files dropped it to 1:58, a 50 second savings.  Essentially my screen updates (which call application.processmessages) is slowing down the scanning speed. 

Thanks for your help!

3delite

Good to hear that You managed to speed up the scanning process!

So from my side, here's an update: Download Tags Library

One thing: Ogg Vorbis and Opus playtime parsing is off by default. If You need playtime values for these files enable it, so now the fastest parameters will be:

    Tags := TTags.Create;
    try
        Tags.OggVorbisAndOpusTag.ParsePlayTime := True;
        Tags.ParseCoverArts := False;
        Tags.ID3v2Tag.MPEGSearchLength := 0;
        Tags.LoadFromFile(FileName);
        //* Code here...
    finally
        FreeAndNil(Tags);
    end;

With the update MP4 and ID3v2 cover art bytes are completely skipped and not read from the file.
Important: do not save the tags when using "Tags.ParseCoverArts := False;" that would corrupt the files, it can only be used for loading tags.

If You get playtime issues please send the problematic file, and I'll see what I can do!

Thanks!