How to Read / Write ID3 Tags in MP3 Files

Posted on December 15th, 1999 in General | 1 Comment »

The thing that makes MPEG Layer 3 files good (besides their size:) are ID3 tags. Thanks to them you can save information about the song. Here’s the ID3 tag structure and information on reading/modifying them. Enjoy!
The ID3 tag is saved in the last 128 bytes of a MPEG Layer 3 file. It starts with a “TAG” string. If this string is absent that means that ID3 information has been removed. But don’t worry – all you have to do is append it to the file.
In newer versions of Delphi if you are going to use this code you may want to use the packed keyword with arrays and records.
The MPEG Layer 3 ID3 tag structure:

  ID3Struct = record
    Signature: array[0..2] of Char; { Should be: "TAG" }
    Title,
    Artist,
    Album: array[0..29] of Char;
    Year: array[0..3] of Char;
    Comment: array[0..29] of Char;
    Genre: Byte;
  end;

Here’s the genre (Max. 256 entries).

  ID3Genre: array[0..126] of string = (
    'Blues', 'Classic Rock', 'Country', 'Dance', 'Disco', 'Funk', 'Grunge',
    'Hip-Hop', 'Jazz', 'Metal', 'New Age', 'Oldies', 'Other', 'Pop', 'R&B',
    'Rap', 'Reggae', 'Rock', 'Techno', 'Industrial', 'Alternative', 'Ska',
    'Death Metal', 'Pranks', 'Soundtrack', 'Euro-Techno', 'Ambient',
    'Trip-Hop', 'Vocal', 'Jazz+Funk', 'Fusion', 'Trance', 'Classical',
    'Instrumental', 'Acid', 'House', 'Game', 'Sound Clip', 'Gospel',
    'Noise', 'AlternRock', 'Bass', 'Soul', 'Punk', 'Space', 'Meditative',
    'Instrumental Pop', 'Instrumental Rock', 'Ethnic', 'Gothic',
    'Darkwave', 'Techno-Industrial', 'Electronic', 'Pop-Folk',
    'Eurodance', 'Dream', 'Southern Rock', 'Comedy', 'Cult', 'Gangsta',
    'Top 40', 'Christian Rap', 'Pop/Funk', 'Jungle', 'Native American',
    'Cabaret', 'New Wave', 'Psychadelic', 'Rave', 'Showtunes', 'Trailer',
    'Lo-Fi', 'Tribal', 'Acid Punk', 'Acid Jazz', 'Polka', 'Retro',
    'Musical', 'Rock & Roll', 'Hard Rock', 'Folk', 'Folk-Rock',
    'National Folk', 'Swing', 'Fast Fusion', 'Bebob', 'Latin', 'Revival',
    'Celtic', 'Bluegrass', 'Avantgarde', 'Gothic Rock', 'Progressive Rock',
    'Psychedelic Rock', 'Symphonic Rock', 'Slow Rock', 'Big Band',
    'Chorus', 'Easy Listening', 'Acoustic', 'Humour', 'Speech', 'Chanson',
    'Opera', 'Chamber Music', 'Sonata', 'Symphony', 'Booty Bass', 'Primus',
    'Porn Groove', 'Satire', 'Slow Jam', 'Club', 'Tango', 'Samba',
    'Folklore', 'Ballad', 'Power Ballad', 'Rhythmic Soul', 'Freestyle',
    'Duet', 'Punk Rock', 'Drum Solo', 'Acapella', 'Euro-House', 'Dance Hall'
  );

This is the ID3 Tag read code:

var
  fMP3: file of Byte;
  Tag: ID3Struct;
begin
  try
    (* sFileName - (string) The full file name with path. *)
    AssignFile(fMP3, sFileName);
    Reset(fMP3);
    try
      Seek(fMP3, FileSize(fMP3) - 128);
      BlockRead(fMP3, Tag, SizeOf(Tag));
    finally
    end;
  finally
    CloseFile(fMP3);
  end;
  if fMP3.Signature <> 'TAG' then begin
    ...
    { Doesn't have an ID3 tag }
  end else begin
    ...
    { Do something with the tag }
  end;
end;

(* WriteID3Tag() function
**
** Copyright (c) 2000 Jacob Dybala (m3Rlin)
** Freeware.
**
** Created : January 7 2000
** Modified: January 7 2000
** Please leave this copyright notice.
*)
procedure WriteID3Tag(id3NewTag: ID3Struct; sFileName: string);
var
  fMP3: file of Byte;
  Tag : ID3Struct;
begin
  try
    AssignFile(fMP3, sFileName);
    Reset(fMP3);
    try
      Seek(fMP3, FileSize(fMP3) - ID3OffsetFromEnd);
      BlockRead(fMP3, Tag, SizeOf(Tag));
      if fMP3.Signature = 'TAG' then
        { Replace old tag }
        Seek(fMP3, FileSize(fMP3) - ID3OffsetFromEnd)
      else
        (* Append tag to file because it doesn't exist.
        * Cannot use Append() function. It's only for text files.
        *)
        Seek(fMP3, FileSize(fMP3));
      BlockWrite(fMP3, id3NewTag, SizeOf(id3NewTag));
    finally
    end;
  finally
    CloseFile(fMP3);
  end;
end;

How to Base 64 (MIME) Encode and Decode a String

Posted on December 15th, 1999 in General | 3 Comments »

MIME encoding and decoding is used when transferring binary files over the Internet. Every byte is converted to an ASCII character that can be transfered with no problems. This is the code to encode and decode using Base 64 (MIME). You can TMemo’s and TRichEdit’s string text via the Text property. The original code has been written by David Barton davebarton@bigfoot.com but I have optimized it a lot (for size). I have put in all the for loops and case statements.

uses
  SysUtils;
...
const
  B64Table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
...
function Base64Encode(const S: string): string;
var
  InBuf: array[0..2] of Byte;
  OutBuf: array[0..3] of Char;
  iI, iJ: Integer;
begin
  SetLength(Result, ((Length(S) + 2) div 3) * 4);
  for iI := 1 to ((Length(S) + 2) div 3) do begin
    if Length(S) < (iI * 3) then
      Move(S[(iI - 1) * 3 + 1], InBuf, Length(S) - (iI - 1) * 3)
    else
      Move(S[(iI - 1) * 3 + 1], InBuf, 3);
    OutBuf[0] := B64Table[((InBuf[0] and $FC) shr 2) + 1];
    OutBuf[1] := B64Table[(((InBuf[0] and $3) shl 4) or ((InBuf[1] and $F0) shr 4)) + 1];
    OutBuf[2] := B64Table[(((InBuf[1] and $F) shl 2) or ((InBuf[2] and $C0) shr 6)) + 1];
    OutBuf[3] := B64Table[(InBuf[2] and $3F) + 1];
    Move(OutBuf, Result[(iI - 1) * 4 + 1], 4);
  end;
  if Length(S) mod 3 = 1 then begin
    Result[Length(Result) - 1] := '=';
    Result[Length(Result)] := '=';
  end else if Length(S) mod 3 = 2 then
    Result[Length(Result)] := '=';
end;

function Base64Decode(const S: string): string;
var
  OutBuf: array[0..2] of Byte;
  InBuf : array[0..3] of Byte;
  iI, iJ: Integer;
begin
  if Length(S) mod 4 <> 0 then
    raise Exception.Create('Base64: Incorrect string format');
  SetLength(Result, ((Length(S) div 4) - 1) * 3);
  for iI := 1 to (Length(S) div 4) - 1 do begin
    Move(S[(iI - 1) * 4 + 1], InBuf, 4);
    for iJ := 0 to 3 do
      case InBuf[iJ] of
        43: InBuf[iJ] := 62;
        48..57: Inc(InBuf[iJ], 4);
        65..90: Dec(InBuf[iJ], 65);
        97..122: Dec(InBuf[iJ], 71);
      else
        InBuf[iJ] := 63;
      end;
    OutBuf[0] := (InBuf[0] shl 2) or ((InBuf[1] shr 4) and $3);
    OutBuf[1] := (InBuf[1] shl 4) or ((InBuf[2] shr 2) and $F);
    OutBuf[2] := (InBuf[2] shl 6) or (InBuf[3] and $3F);
    Move(OutBuf, Result[(iI - 1) * 3 + 1], 3);
  end;
  if Length(S) <> 0 then begin
    Move(S[Length(S) - 3], InBuf, 4);
    if InBuf[2] = 61 then begin
      for iJ := 0 to 1 do
        case InBuf[iJ] of
          43: InBuf[iJ] := 62;
          48..57: Inc(InBuf[iJ], 4);
          65..90: Dec(InBuf[iJ], 65);
          97..122: Dec(InBuf[iJ], 71);
        else
          InBuf[iJ] := 63;
        end;
      OutBuf[0] := (InBuf[0] shl 2) or ((InBuf[1] shr 4) and $3);
      Result := Result + Char(OutBuf[0]);
    end else if InBuf[3] = 61 then begin
      for iJ := 0 to 2 do
        case InBuf[iJ] of
          43: InBuf[iJ] := 62;
          48..57: Inc(InBuf[iJ], 4);
          65..90: Dec(InBuf[iJ], 65);
          97..122: Dec(InBuf[iJ], 71);
        else
          InBuf[iJ] := 63;
        end;
      OutBuf[0] := (InBuf[0] shl 2) or ((InBuf[1] shr 4) and $3);
      OutBuf[1] := (InBuf[1] shl 4) or ((InBuf[2] shr 2) and $F);
      Result := Result + Char(OutBuf[0]) + Char(OutBuf[1]);
    end else begin
      for iJ := 0 to 3 do
        case InBuf[iJ] of
          43: InBuf[iJ] := 62;
          48..57: Inc(InBuf[iJ], 4);
          65..90: Dec(InBuf[iJ], 65);
          97..122: Dec(InBuf[iJ], 71);
        else
          InBuf[iJ] := 63;
        end;
      OutBuf[0] := (InBuf[0] shl 2) or ((InBuf[1] shr 4) and $3);
      OutBuf[1] := (InBuf[1] shl 4) or ((InBuf[2] shr 2) and $F);
      OutBuf[2] := (InBuf[2] shl 6) or (InBuf[3] and $3F);
      Result := Result + Char(OutBuf[0]) + Char(OutBuf[1]) + Char(OutBuf[2]);
    end;
  end;
end;