Read and write ID3Tag in Delphi without any extra components [updated]

There's an (probably very) old About.com Delphi's article on how to build simply MP3 Player using only TMediaPlayer component available in every Delphi edition. Since powerful XAudio is no longer offcially available to Windows developers and other MP3 players or components are quite expensive, when it comes to pay for the license, then this could be a good alternative. But, this article catched my attention for a completely different reason -- reading and writing ID3Tag v1 in MP3 files, without using external libraries, just the pure Delphi code.

Introduction

Before jumping over solution, about which I'm talking here, I was using MPGTools library, which does worked fine and provided many more posibillities (like full tags manipulation, reading audio file duration and many more). Unfortunately, the last version I found was from 1999 (fourteen years old!) and Delphi 5 was the last version of Delphi, that compiled it. When I switched to Delphi 7, to turned out, that it refuses to compile projects with MPGTools.pas -- there were too many "unsafe code" warnings and other, higher level errors. This caused me to look for another solution, and I found this one.

Page number four of mentioned article contains simple example on how to read and write ID3Tags in MP3 files using just Delphi routines and simple binary operations. That was quite interesting for me, since before reading this article I had to use special component or library for this purpose.

The example is not only very old, but also a little bit buggy. So, here are two quick fixes, if you want to use it.

Fix #1: ID3Tag record correction

First of all, I don't know, from where authors took his ID3Tag record definition, but it is wrong. You should definitely change TID3Rec record declaration, found somewhere in the beginning of Unit1.pas to (thanks to MPGTools.pas!):

TID3Rec = packed record
    Tag : Array[1..3] of char;         { If tag exists this must be 'TAG' }
    Title : Array[1..30] of char;      { Title data (PChar) }
    Artist : Array[1..30] of char;     { Artist data (PChar) }
    Album : Array[1..30] of char;      { Album data (PChar) }
    Year : Array[1..4] of char;        { Date data }
    Comment : Array[1..30] of char;    { Comment data (PChar) }
    Genre : Byte;                      { Genre data }
end;

If you leave original one, ID3Tag reading function will work incorrectly (writning one probably as well), giving you Year and Comment field glued together in the Album field, with Year field empty and Comment field filled with actual album.

Fix #2: Avoiding AccessViolation

Second change is required in TForm1.mp3ListClick function. You should move mp3player.Close function call before FillID3TagInformation function call. If you leave it in original place (after) you'll get AccessViolation each time, when you click second time already selected file on the list (this is of course due to FillID3TagInformation function attempting to read ID3Tag from the file already locked by TMediaPlayer).

More about ID3Tag reading

I did also some other minor fixes and changes (like reading track number -- see below) to this example project. Fixed version of example project is available here for download.

Fine! What about track number, you may ask. Well, it's not there, because it is not stored directly as separate field only as last byte of comment field. Don't blame me, I've seen more stupid things! :] So, to get to track number, you have to simple do this magic:

Track.Text := IntToStr(Ord(ID3.Comment[30]));

Notice, that there isn't an easy way of reading MP3 file duration expressed in minutes and seconds, since it is not stored in the file, neither in ID3Tag nor anywhere else. If you dig through MPGTools.pas source code, you'll find that it is calculated using many input variables (like header and frame size) and with complex functions.

Now, since we're using direct, binary operation of files here (like BlockRead and BlockWrite), you'll be also plunged with a lot of "Unsafe code" warnings in Delphi 7 (and probably all newer versions). But, if you ignore them, project should compile and ID3Tag v1 reading and writing should work like a charm.

Writing ID3Tags

That's mostly all about reading tags. Now, we need to say something about writing it. Presented example (both provided modified version and original one under above mentioned URL) does not write ID3Tag, but has proper function for this. So dig-in, if you need this.

What you have to keep in mind (what is not mentioned in the example) is that most fields in ID3Tag are arrays of char, not strings. While you most problably use strings (for example TEdit.Text property) to store values, you want to write to ID3Tag. For this, you need to do a little bit more magic, than simple assignment of values. Take a look at this example:

var
    newID3: TID3Rec;
    sTitle, sArtist, sAlbum, sYear, sComment, sGenre, sTrack: String;
begin
    FillChar(NewID3, SizeOf(NewID3), 0);
    NewID3.Tag := 'TAG';
    Move(sTitle[1], NewID3.Title, Length(sTitle));
    Move(sArtist[1], NewID3.Artist, Length(sArtist));
    Move(sAlbum[1], NewID3.Album, Length(sAlbum));
    Move(sYear[1], NewID3.Year, Length(sYear));
    Move(sComment[1], NewID3.Comment, Length(sComment));
    NewID3.Genre := GetGenre(sGenre);
    NewID3.Comment[30] := Chr(StrToIntDef(sTrack, 0));
end;

Since genre is most likely displayed as string:

if ID3.Genre > MaxID3Genre then ID3.Genre := MaxID3Genre;
sGenre := ID3Genre[ID3.Genre];

and is stored as byte, must be converted before writing ID3Tag. If you're using consts from example (MaxID3Genre integer and ID3Genre array), function for doing so is as easy as:

function GetGenre(genreStr: String): Integer;
var
    a: Integer;
begin
    Result := MaxID3Genre;

    for a := 0 to MaxID3Genre - 1 do if ID3Genre[a] = genreStr then Result := a;
end;

When you prepare newID3 like that, it will be ready to be used as parameter of ChangeID3Tag function call to actually write ID3Tag.

Conclusion

For the reference: here the entire article (four pages with a lot of unnecessary blah, blah, blah) and here is a link to last, fifth page, which contains full source code, both zipped and freely written.

Notice, that above mentioned article was written before ID3Tagv2 was ever introduced, so keep in mind, that we're talking here about much simpliers, fixed-length tag information only. If you would like to work also with ID3Tagv2, then I suggest looking around answers to my question posted at StackOverflow. You'll find there a ready solution (pure Delphi code) to remove all ID3Tagv2 data (which is, what I was asking for) plus links to Delphi libs, that offers full editing capabilities.


Update: Few days after writing this article, I found out, that all you have to undertake to compile MPGTools.pas under Delphi 7 is to change Assign into AssignFile in two lines. But, since I don't want to use whole big library just to read or write ID3Tagv1, so I'm still using above presented solution.

Leave a Reply