ホーム >プログラム >Delphi 6 ローテクTips

ID3タグ自作関数 ID3v2読み込み

お手軽ID3v2読み込みではタグヘッダーやフレームヘッダーのフラグを無視して実装しました。
それでも充分実用になるとは思うのですが、せっかくなので細かい仕様にも対応してみましょうか、というページ。
ID3v2の基本的な読み込みに関してはお手軽版を参照。


参考サイト


タグヘッダ


ID3v2ヘッダ 10バイト固定
オフセット 長さ(バイト) 内容
0 3 'ID3' の識別文字3文字
3 2 バージョン
5 1 フラグ
6 4 サイズ(Syncsafe)
ID3v2ヘッダの例
オフセット 内容の例 説明
0 x49 I 識別子
「ID3」で固定
1 x44 D
2 x33 3
3 x03 3 メジャーバージョン バージョンは2.3.0
4 x00 0 リビジョン
5 x00 0 フラグ(この例ではフラグはなし)
6 x00 4086バイト Syncsafeな数値なので8054バイトではなく4086バイトになる。
x1F * x80 + x76 = xFF6
 31 * 128 + 118 = 4086
7 x00
8 x1F
9 x76
10〜4095 この例では拡張ヘッダはないのでこれ以降の4086バイトはフレームデータ(と、もしあればパディング領域)になる。
拡張ヘッダがある場合はこの4086バイトに拡張ヘッダも含まれる。
4096〜 これ以降mp3本体
4096というのは(10 + 4086)
ヘッダの10バイトとヘッダサイズの4086バイト。
type
{タグヘッダ}
  //拡張ヘッダ 2.3以降
  TMyID3HeaderExtended = record
    iSize:        DWORD;
    iPaddingSize: DWORD;    //2.3のみ
    bCrc:         Boolean;
    iCrcData:     DWORD;
  end;
  //ヘッダフラグ
  TMyID3HeaderFlag = record
    bUnsync:       Boolean;
    bCompress:     Boolean; //2.2のみ
    bExtended:     Boolean; //2.3〜
    bExperimental: Boolean; //2.3〜
    bFooter:       Boolean; //2.4のみ
  end;
  //ヘッダ
  TMyID3Header = record
{*} iSize:     DWORD;
    rFlag:     TMyID3HeaderFlag;
    rExtended: TMyID3HeaderExtended;
  end;
const
  lciTAG_HEADERSIZE = 10;  //ヘッダのサイズは10バイト固定


procedure TMyTagID3.F_ReadHeader;
var
  lp_Buff: PAnsiChar;
  li_Flag: Integer;
  li_Size: DWORD;
begin
  FillChar(F_rHeader, SizeOf(F_rHeader), 0);
  li_Size := gfniFileRead(lp_Buff, F_sFileName, lciTAG_HEADERSIZE);  //10バイト固定
  try
    if (li_Size = lciTAG_HEADERSIZE) then begin
      if  (lp_Buff[0] = 'I')
      and (lp_Buff[1] = 'D')
      and (lp_Buff[2] = '3')
      then begin
        F_iVer   := 2;  //最初の3バイトが'ID3'ならID3v2
        F_iMajor := Ord(lp_Buff[3]);
        F_iRev   := Ord(lp_Buff[4]);
        li_Flag  := Ord(lp_Buff[5]);
        F_rHeader.iSize := F_GetSyncsafeSize(@lp_Buff[6], 4);
        //サイズにはヘッダー自身の10バイトとフッターの10バイトは含まない

        F_rHeader.rFlag.bUnsync := ByteBool(li_Flag and $80);         //7bit 非同期化処理されているか
        if (F_iMajor = 2) then begin
          F_rHeader.rFlag.bCompress := ByteBool(li_Flag and $40);     //6bit 2.2のみ 圧縮
        end else if (F_iMajor >= 3) then begin
          F_rHeader.rFlag.bExtended     := ByteBool(li_Flag and $40); //6bit 2.3〜 拡張ヘッダーがあるか
          F_rHeader.rFlag.bExperimental := ByteBool(li_Flag and $20); //5bit 2.3〜 テスト用か
          if (F_iMajor >= 4) then begin
            F_rHeader.rFlag.bFooter := ByteBool(li_Flag and $10);     //4bit 2.4 フッターがあるか
          end;
        end;
      end;
    end;
  finally
    FreeMem(lp_Buff);
  end;
end;

タグヘッダのフラグは5種類あります。

  1. 7ビット目(左端)
    タグ全体が非同期化処理されているか
    このフラグが立っていた場合、非同期化処理を解除してからフレームを読まないとうまくいきません。
    非同期化処理の解除というと難しそうに思いますが、実際はそれほど難しくありません。
    頭から1バイトずつみていき、$FFのあとに続く$00を取り除いていくという、それだけのことです。
    function TMyTagID3.F_DecodeUnsyncData(pData: PAnsiChar; iLen: DWORD): DWORD;
    {非同期化処理されたデータを元に戻し、元のサイズを返す
    ヘッダーのフラグで非同期化フラグが立っている場合、ヘッダーのデータ中に同期信号と同
    じパターンが現れるのを避けるための処理が行われているのでそれを元に戻す。

    回避処理の内容は、同期信号のパターンが2進表記で
    %11111111 111xxxxx
    となるので xFF の後に x00 を挿入して
    %11111111 00000000 111xxxxx
    とする。これで同期信号と同じパターンを回避する。
    回避処理の内容は頭からデータを1バイトづつ見ていき xFF に出くわし、その後の1バイト
    が xE0 以上または x00 なら x00 を加える。または単純に xFF の後に x00 を加える。

    このルーチンではその逆を行うため xFF の次に現れる x00 を取り除く処理を行う。
    この結果ヘッダーの実際のサイズは取り除いた x00 の分だけ減る。
    }

    var
      i, li_Shift: DWORD;
      lp_Pos: PAnsiChar;
    begin
      lp_Pos := pData;
      li_Shift := 0;

      for i := 0 to iLen - (1 + li_Shift) do begin
        if  (i > 0)
        and (lp_Pos[-1] = #$FF)
        and (lp_Pos[0]  = #$0)
        then begin
          //非同期化処理されたパターン
          Inc(li_Shift);
          Inc(lp_Pos);
          lp_Pos[-li_Shift] := lp_Pos[0]; //削除した分を詰める
        end else if (li_Shift > 0) then begin
          lp_Pos[-li_Shift] := lp_Pos[0]; //削除した分を詰める
        end;
        Inc(lp_Pos);
      end;
      Result := iLen - li_Shift;
    end;
  2. 6ビット目(左から2番目)
    タグが圧縮されているか (v2.2のみ)
    v2.2では6ビット目が圧縮処理されているかのフラグになっています。
    が、どうもv2.2制定時点ではまだ圧縮フォーマットが決定していなかったようで、このフラグが立っていたらタグは読めないはずなので諦めましょうということのようです。
    そしてv2.3以降ではこのフラグは下記のように別の意味になってしまっています。
    なんだかなぁ、、
  3. 6ビット目(左から2番目)
    拡張ヘッダーがあるか (v2.3以降)
    このフラグが立っている場合、タグヘッダの10バイトの後にはフレームではなく、拡張タグヘッダがついています。
  4. 5ビット目(左から3番目)
    実験中のタグか (v2.3以降)
    このフラグが立っていた場合、タグは実験段階にあるものとみなしてくださいということのようです。
    生暖かい目で見守ってやってくださいってやつでしょうか。
  5. 4ビット目(左から4番目)
    フッタがあるか (v2.4)
    タグの最後に10バイトのフッタがついているかどうかのフラグです。
    ちなみにタグヘッダに記されているタグサイズにはタグヘッダ自身の10バイトとこのフッタの10バイトは含まれません。

拡張タグヘッダ

v2.3以降で拡張タグヘッダのフラグが立っている場合拡張タグヘッダの解析が必要になります。
とはいえタグの読み込みだけであればその内容に関しては無視して問題ありません。
ただ、フレームの頭出しのため拡張タグヘッダを読み飛ばすためのサイズ情報が必要になります。
この拡張タグヘッダはタグヘッダ中のサイズ(TMyID3HeaderレコードのiSizeメンバ)に含まれるので、フレームデータと一緒に読み込んで処理してしまいます。

procedure TMyTagID3.F_ReadV2;
var
  lp_Buff: PAnsiChar;
  li_Size, li_Offset: DWORD;
  ls_Text: WideString;
begin
  if (F_rHeader.iSize > 0) then begin
    //10バイト目からタグサイズ分を読み込む。
    //lp_Buffには拡張タグヘッダとフレームデータがある。

    li_Size := gfniFileRead(lp_Buff, F_sFileName, lciTAG_HEADERSIZE, F_rHeader.iSize);
    try
      if (li_Size = F_rHeader.iSize) then begin
        li_Offset := 0;

最初に、タグヘッダのフラグで非同期化処理フラグが立っていたら非同期化解除処理を行います。

        if (F_rHeader.rFlag.bUnsync) then begin
          //非同期化処理を行ってあるので元に戻し、戻した後のサイズを返す
          F_rHeader.iSize := F_DecodeUnSyncData(lp_Buff, li_Size);
        end;

次に拡張タグヘッダを読み飛ばすために拡張タグヘッダのサイズを取得します。

        if (F_rHeader.rFlag.bExtended) then begin
          //拡張ヘッダのサイズを取得
          F_rHeader.rExtended.iSize := F_GetSize(lp_Buff, 4);
          if (F_iMajor = 3) then begin
            //2.3
            F_rHeader.rExtended.iPaddingSize := F_GetSize(@lp_Buff[6], 4);
            F_rHeader.rExtended.bCrc         := ByteBool(Ord(lp_Buff[4]) and $80);
            if (F_rHeader.rExtended.bCrc) then begin
              F_rHeader.rExtended.iCrcData := F_GetSize(@lp_Buff[10], 4);
            end;
            Dec(F_rHeader.iSize, F_rHeader.rExtended.iPaddingSize);  //パディング分を引いておく
            {
            拡張ヘッダサイズ(rExtended.iSize)には拡張ヘッダサイズを表す4バイトは含まれない。
            そのためrExtended.iSizeは6か10のどちらかとなる。
            6というのはCRCフラグがオフの場合でパディングサイズを表す4バイトとフラグの2バイト。
            10というのはCRCフラグがオンで上記の6バイトにCRCデータの4バイト。
            で、結局フレームを読むためのオフセットはヘッダーサイズを表した4バイト + rExtended.iSize となる。
            }

            li_Offset := 4 + F_rHeader.rExtended.iSize;
          end else if (F_iMajor = 4) then begin
            //2.4
            //2.4は2.3と違い拡張ヘッダーサイズを表す4バイトも含んだサイズ。
            //なのでオフセットは単純にrExtended.iSizeを足すだけ。
            li_Offset := F_rHeader.rExtended.iSize;
            //拡張ヘッダーの詳細は読み込みには必要ないので割愛
          end;
        end;

拡張ヘッダのサイズはv2.3とv2.4では求め方が違っています。

ID3v2ヘッダ、拡張タグありの例(ver2.3.0)
オフセット 内容の例 説明
0 x49 I 識別子
「ID3」で固定
1 x44 D
2 x33 3
3 x03 3 メジャーバージョン バージョンは2.3.0
4 x00 0 リビジョン
5 x60
1 1 0 0 0 0 0 0
フラグ(この例では非同期化処理されていて、拡張タグヘッダあり)
6 x00 4086(バイト) Syncsafeな数値なので8054バイトではなく4086バイトになる
x1F * x80 + x76 = xFF6
 31 * 128 + 118 = 4086
7 x00
8 x1F
9 x76
10 x00 6(バイト) 拡張タグのサイズ。
この例ではCRCデータはないのでサイズには6が入る。
CRCデータがある場合は10が入る。
11 x00
12 x00
13 x06
14 x00 拡張フラグ
15 x00
16 x00 パディング領域のサイズ
17 x00
18 x0D
19 xC6
20〜4095 これ以降フレームデータとパディング領域
4096〜 これ以降mp3本体
4096というのは(10 + 4086)ヘッダの10バイトとヘッダサイズの4086バイト

フレームヘッダ

ID3v2ヘッダ、フレームヘッダの例(拡張ヘッダなし)
オフセット 内容の例 説明
0 x49 'I' 識別子
「ID3」で固定
1 x44 'D'
2 x33 '3'
3 x03 3 メジャーバージョン バージョンは2.3.0
4 x00 0 リビジョン
5 x00 0 フラグ(この例ではなし)
6 x00 4086(バイト) Syncsafeな数値なので8054バイトではなく4086バイトになる
x1F * x80 + x76 = xFF6
 31 * 128 + 118 = 4086
7 x00
8 x1F
9 x76
10 x54 'T' フレームID
この例では「TIT2」(タイトル)
11 x49 'I'
12 x54 'T'
13 x32 '2'
14 x00 19バイト フレームサイズ
フレームヘッダの10バイトは含まないサイズ
この例ではver2.3.0なのでサイズは4バイトであらわし、非同期化処理はされないので19バイトの長さ
15 x00
16 x00
17 x13
18 x00 0 ステータスフラグ
19 x00 0 フォーマットフラグ(ここまでがフレームヘッダ)
20 x01 1 文字コード(この例ではBOMありのUnicodeを指定)
21 xFF UnicodeのBOM この例ではリトルエンディアンのBOM
22 xFE
23〜38



タイトル(Unicodeなので、(19-3)/2 の8文字)
終端文字はあってもなくても良い
39 x54 'T' 次のフレームヘッダ
フレームID
「TRCK」
40 x52 'R'
41 x43 'C'
42 x4B 'K'
43 x00 2バイト フレームサイズ
この例では2バイトの長さ
44 x00
45 x00
45 x02
47 x00 0 ステータスフラグ
48 x00 0 フォーマットフラグ(ここまでがフレームヘッダ)
49 x00 0 文字コード(Shift-JIS)
50 x31 '1' トラック'1'
51 x55 'U' 次のフレーム
非同期歌詞(改行ありのテキスト)の例
52 x53 'S'
53 x4C 'L'
54 x54 'T'
55 x00 682バイト 682バイトの長さ
この長さには文字コード、言語コード、簡単な説明、簡単な説明の終端文字、(Unicodeなら)BOM、歌詞本体の長さの全部が含まれる
56 x00
57 x02
58 xAA
59 x00 0 ステータスフラグ
60 x00 0 フォーマットフラグ
61 x01 1 文字コード(BOMありのUnicode)
62 x6A 'j' 言語コード
「jpn」(日本語)
63 x70 'p'
64 x6E 'n'
65 xFF UnicodeのBOM 簡単な説明のBOM
この例では簡単な説明は空文字
66 xFE
67 x00
簡単な説明の終端文字
Unicodeなのでx00が二つ
68 x00
69 xFF UnicodeのBOM 歌詞本体のBOM
70 xFE
71〜742



歌詞本体(この例では長さは (682-(1+3+2+2+2))/2 で366文字)
終端文字はあってもなくて良い
改行はコードはLF(x1A)のみ
743〜4095

以下同様にフレームが続く
4096〜 これ以降mp3本体


フレームヘッダフラグ

拡張タグヘッダの処理ができたところでフレームの読み込みになるのですが、その前に今度はフレームのフラグについて解析します。

フレームヘッダフラグは2バイトあり、フレームの状態を表すステータスフラグとフレームのフォーマットに関するフォーマットフラグの二つに分かれます。
1バイト目がステータスフラグ、その後の1バイトがフォーマットフラグになります。

type
{フレーム}
  //フレームのステータスフラグ
  TMyID3FrameStatusFlag = record
    bDelFromTag:      Boolean;  //タグを変更したときにこのフレームを削除すべきか
    bDelFromFile:     Boolean;  //タグ以外のデータを変更したときにこのフレームを削除すべきか
    bReadOnly:        Boolean;  //読み込み専用か
  end;
  //フレームのフォーマットフラグ
  TMyID3FrameFormatFlag = record
    bGroup:           Boolean;  //グループ化されているか
      iGroupID:       Byte;     //  グループ識別子
    bCompress:        Boolean;  //圧縮されているか
      iDecompSize:    DWORD;    //圧縮伸張後のサイズ
    bEncrypt:         Boolean;  //暗号化されているか
      iEncryptMethod: Byte;     //  暗号化の方法
    bUnsync:          Boolean;  //非同期化されているか
    bDataLen:         Boolean;  //フラグが何もない場合のフレームサイズのデータが付加されているか
      iDataLen:       DWORD;    //  フラグが何もない場合のフレームサイズ
  end;

  //フレームヘッダー
  TMyID3Frame = record
    sFrameID:    String[4]; //フレームを判別するための'0'..'9','A'..'Z'で構成されている識別子
    iHeaderSize: Byte;       //ヘッダーのサイズ 2.2とそれ以降(2.3〜)で違う
    iSize:       DWORD;      //フレームサイズ
    iReadSize:   DWORD;      //正味のフレームサイズ
    rStatusFlag: TMyID3FrameStatusFlag;
    rFormatFlag: TMyID3FrameFormatFlag;
  end;

そのフレームヘッダフラグは、各バージョンごとで仕様が異なっています。

procedure TMyTagID3.F_ReadFrame(pData: PAnsiChar);
  function lfni_GetSize(pFrameData: PAnsiChar; iByte: Byte): Integer;
  begin
    Result := F_GetSize(@pFrameData[F_rFrame.iHeaderSize], iByte);
    Inc(F_rFrame.iHeaderSize, iByte);
    Dec(F_rFrame.iSize,       iByte);
  end;
begin
  FillChar(F_rFrame, SizeOf(F_rFrame), 0);

  //フレームIDが正しくない場合はフレームヘッダのサイズは0のまま
  if (pData[0] in ['0'..'9', 'A'..'Z']) then begin
    if (F_iMajor = 2) then begin
      //Ver2.2
      F_rFrame.sFrameID := gfnsByteCopy(pData, 0, 3);
      //ID、サイズとも3バイトでフラグのヘッダサイズは6バイト
      F_rFrame.iHeaderSize := 3 + 3;
      F_rFrame.iSize       := F_GetSize(@pData[3], 3);
      F_rFrame.iReadSize   := F_rFrame.iSize;
    end else if (F_iMajor >= 3) then begin
      //Ver2.3以上
      F_rFrame.sFrameID := gfnsByteCopy(pData, 0, 4);
      //ID、サイズとも4バイト、2バイトのフラグでフラグのヘッダサイズは10バイト
      F_rFrame.iHeaderSize := 4 + 4 + 2;
      if (F_iMajor = 3) then begin
        //Ver2.3
        F_rFrame.iSize := F_GetSize(@pData[4], 4);
        F_rFrame.rStatusFlag.bDelFromTag  := ByteBool(Ord(pData[8]) and $80);  //タグが変更されたらこのフレームを削除すべきか
        F_rFrame.rStatusFlag.bDelFromFile := ByteBool(Ord(pData[8]) and $40);  //本体データの一部が変更されたらこのフレームを削除すべきか(データが完全に置き換えられた場合は関係ない)
        F_rFrame.rStatusFlag.bReadOnly    := ByteBool(Ord(pData[8]) and $20);  //読み取り専用か
        F_rFrame.rFormatFlag.bCompress    := ByteBool(Ord(pData[9]) and $80);  //圧縮されているか
        F_rFrame.rFormatFlag.bEncrypt     := ByteBool(Ord(pData[9]) and $40);  //暗号化されているか
        F_rFrame.rFormatFlag.bGroup       := ByteBool(Ord(pData[9]) and $20);  //グループ化されているか

        //以降の順番は変えてはいけない
        if (F_rFrame.rFormatFlag.bCompress) then F_rFrame.rFormatFlag.iDecompSize    := lfni_GetSize(pData, 4);
        if (F_rFrame.rFormatFlag.bEncrypt)  then F_rFrame.rFormatFlag.iEncryptMethod := lfni_GetSize(pData, 1);
        if (F_rFrame.rFormatFlag.bGroup)    then F_rFrame.rFormatFlag.iGroupID       := lfni_GetSize(pData, 1);
        F_rFrame.iReadSize := F_rFrame.iSize;
      end else begin
        //Ver2.4以上
        F_rFrame.iSize := F_GetSyncsafeSize(@pData[4], 4);
        F_rFrame.rStatusFlag.bDelFromTag  := ByteBool(Ord(pData[8]) and $40); //タグ変更でこのフレームを削除するか
        F_rFrame.rStatusFlag.bDelFromFile := ByteBool(Ord(pData[8]) and $20); //本体データの変更でこのフレームを削除するか
        F_rFrame.rStatusFlag.bReadOnly    := ByteBool(Ord(pData[8]) and $10); //読み取り専用か
        F_rFrame.rFormatFlag.bGroup       := ByteBool(Ord(pData[9]) and $40); //グループ化
        F_rFrame.rFormatFlag.bCompress    := ByteBool(Ord(pData[9]) and $8);  //圧縮(v2.3と違いフレームヘッダの後に圧縮前のサイズは追加されていない)
        F_rFrame.rFormatFlag.bEncrypt     := ByteBool(Ord(pData[9]) and $4);  //暗号化
        F_rFrame.rFormatFlag.bUnsync      := ByteBool(Ord(pData[9]) and $2);  //非同期化処理
        F_rFrame.rFormatFlag.bDataLen     := ByteBool(Ord(pData[9]) and $1);  //データ長指定子

        //最初に非同期化処理解除行う
        if (F_rFrame.rFormatFlag.bUnsync) then begin
          F_rFrame.iReadSize := F_DecodeUnsyncData(@pData[F_rFrame.iHeaderSize], F_rFrame.iSize);
        end else begin
          F_rFrame.iReadSize := F_rFrame.iSize;
        end;
        //次に暗号化を解く
        if (F_rFrame.rFormatFlag.bEncrypt) then begin
          //暗号化を解く。。のはまぁ無理なのでこのフラグが立っていたら次のフレームに飛ばすようにした方が良いのかも
        end;
        //最後に圧縮解除
        if (F_rFrame.rFormatFlag.bCompress) then begin
          //圧縮解除
        end;

        //以降の順番は変えてはいけない
        if (F_rFrame.rFormatFlag.bGroup)   then F_rFrame.rFormatFlag.iGroupID       := lfni_GetSize(pData, 1); //グループ化
        if (F_rFrame.rFormatFlag.bEncrypt) then F_rFrame.rFormatFlag.iEncryptMethod := lfni_GetSize(pData, 1); //暗号化
        if (F_rFrame.rFormatFlag.bDataLen) then F_rFrame.rFormatFlag.iDataLen       := lfni_GetSize(pData, 4); //データ長指定子
      end;
    end;
  end;
end;

フレーム本体

拡張タグヘッダーとフレームヘッダーの対応ができました。
あとはフレーム本体を読み込みます。

          if (F_iFrameSize > 0) then begin
            if (F_sFrameID = 'COMM') or (F_sFrameID = 'COM') then begin
              //コメント
              F_sComment := F_GetFullText(@lp_Buff[li_Offset], F_iFrameSize);
            end else if (F_sFrameID = 'USLT') or (F_sFrameID = 'ULT') then begin
              //歌詞
              F_sLylic := F_GetFullText(@lp_Buff[li_Offset], F_iFrameSize);
//            end else if (F_sFrameID = 'TXXXX') or (F_sFrameID = 'TXX') then begin
//              ユーザー定義のテキスト
//              他のTで始まるフレームと違い複数行のテキストなので判定に加える
//              複数行テキストではあるけれどCOMMやUSLTとは少し違うフォーマット
//              F_sUserText := F_GetUserText(@lp_Buff[li_Offset], F_iFrameSize);
            end else if (F_sFrameID = 'SEEK') then begin
              //分割タグ
              Inc(li_Offset, F_GetSize(@lp_Buff[li_Offset], 4)); //分割された後ろのタグへのオフセットを足しこむ
            end else if (F_sFrameID[1] = 'T') then begin
              //改行無しのテキスト
              ls_Text := F_GetText(@lp_Buff[li_Offset], F_iFrameSize);

              if (F_sFrameID = 'TRCK') or (F_sFrameID = 'TRK') then begin
                //トラックの番号/セット中の位置
                F_sTrack := ls_Text;

         中略...

              end else if (F_sFrameID = 'TORY') or (F_sFrameID = 'TOR') then begin
                //オリジナルのリリース年
                F_sOriginalRelease := ls_Text;
              end;
            end;
            //F_iFrameSizeにはフレームごとのサイズが入っているのでオフセットに足しこむ。
            Inc(li_Offset, F_iFrameSize);
          end;
          Inc(li_Offset, F_iFrameHeaderSize);
          Application.ProcessMessages;
        until (li_Offset >= F_iTagSize) or (F_iFrameHeaderSize = 0);
      end;
    finally
      FreeMem(lp_Buff);
    end;
  end;
end;

非同期化処理の解除や圧縮解除を行った後のフレームデータの読み込みはお手軽版と一緒です。

'TXXX'タグは改行を含んだテキストになりますが、COMMやUSLTなどとは少し違うフォーマットになります。
具体的にはCOMMやUSLTにはある言語コードの指定がありません。
それ以外は同じです。

function TMyTagID3.F_GetUserText(pData: PAnsiChar; iCount: DWORD): WideString;
//ユーザー定義文字情報フレーム
var
  i, li_Enc, li_Index: DWORD;
begin
  Result := '';

  li_Enc   := Ord(pData[0]);
  li_Index := 1; //文字エンコード(1Byte)を読み飛ばす
  Dec(iCount);
  if (li_Enc = $0) //ISO-8859-1(Shift-JIS)
  or (li_Enc = $3) //UTF-8
  then begin
    if (iCount > 0) then begin
      //Content Descriptorを読み飛ばす
      for i := 0 to iCount -1 do begin
        if (pData[li_Index + i] = #0) then begin
          Dec(iCount,   (i + 1));
          Inc(li_Index, (i + 1));
          Break;
        end;
      end;

      Result := gfnsByteCopy(pData, li_Index, iCount);
      //必要なら説明文を頭に加える
      //Result := gfnsByteCopy(pData, 0, li_Index - 2) + #13 + Result;
      if (li_Enc = $3) then begin
        //UTF-8
        Result := UTF8Decode(Result);
      end;
    end;
  end else begin
    //Unicode
    //Content Decriptorを読み飛ばす
    for i := 0 to iCount -1 do begin    //iCountは文字数ではなくバイト数
      if  (i mod 2 = 1)                  //Unicodeなので2バイトごとの処理
      and (pData[li_Index + (i -1)] = #0) //終了文字
      and (pData[li_Index + i]      = #0) //Unicodeなので2つで終了
      then begin
        Dec(iCount,   (i + 1));
        Inc(li_Index, (i + 1));
        Break;
      end;
    end;

    if (iCount > 0) then begin
      if (li_Enc = $1) then begin
        //BOMありのUTF-16
        Result := gfnsWStrBCopy(pData, li_Index, iCount, cdUnicodeLE);
      end else begin
        //BOMなしのビッグエンディアン
        Result := gfnsWStrBCopy(pData, li_Index, iCount, cdUTF_16BE);
      end;
    end;
  end;
end;

ソースコード

Ver 1、1.1、2.2、2.3、2.4 の読み込みに対応。
Unicode対応。
ヘッダーフラグやフレームフラグなどの細かい仕様にも対応。
ただしフレームフラグについては以下の制限あり。

interface
uses
  Windows;

type
  //Ver 1
  TMyID3v1 = record
    Ver:     Byte; //ID3v1タグがあれば1、なければ0
    Major:   Byte; //1.0なら0、1.1なら1
    Title:   String[30];
    Author:  String[30];
    Album:   String[30];
    Year:    String[4];
    Comment: String[30];
    Track:   Byte;
    Genre:   Byte;
  end;


type
  //Ver 2
{タグヘッダー}

  //制限情報 %ppqrrstt
  TMyID3HeaderExtendedLimit = record  //書き込み時のみに必要な情報
    iTagSize: Byte;  //タグサイズの制限
{
 00   128 フレーム以下でトータルタグサイズ 1   MB 以下
 01    64 フレーム以下でトータルタグサイズ 128 KB 以下
 10    32 フレーム以下でトータルタグサイズ 40  KB 以下
 11    32 フレーム以下でトータルタグサイズ 4   KB 以下
}

    bCharCode: Boolean;  //文字コードの制限
{
       0    制限なし
       1    ISO-8859-1 [ISO-8859-1] もしくはUTF-8 [UTF-8]のみ使用可
}

    iTextSize: WORD; //テキストフィールドサイズの制限 実際の制限数をセット
{
       00   制限なし
       01   文字列は 1024 文字以内
       10   文字列は  128 文字以内
       11   文字列は   30 文字以内
       これらの文字制限では、文字のバイト数ではなく文字数に言及している点
       に注意すること。最大バイト数は文字コードによって変動する。ひとつの
       テキストフレーム中に、複数の文字列が入っている場合は、それらの合計
       値に対する制限となる。
}

    bPicFormat: Boolean;  //画像フォーマットの制限
{
       0   制限なし
       1   画像はPNG [PNG] もしくは JPEG [JFIF] フォーマットのみ
}

    iPicSize: Byte;  //画像サイズの制限
{
       00  制限なし
       01  全ての画像は256x256 ピクセルかそれ以下
       10  全ての画像は 64x64 ピクセルかそれ以下
       11  特にサイズ指定の無い画像は全て 64x64 ピクセル固定
}

  end;
  //拡張ヘッダー
  TMyID3HeaderExtended = record  //2.3〜
    iSize:        DWORD;
    iPaddingSize: DWORD;    //2.3のみ
    bUpdate:      Boolean;  //2.4
    bCrc:         Boolean;
      iCrcData:   DWORD;
    bLimit:       Boolean;  //2.4
      rLimit:     TMyID3HeaderExtendedLimit;  //2.4
  end;
  //ヘッダーフラグ
  TMyID3HeaderFlag = record
    bUnsync:       Boolean;
    bCompress:     Boolean; //2.2のみ 圧縮されているか
    bExtended:     Boolean; //2.3〜
    bExperimental: Boolean; //2.3〜
    bFooter:       Boolean; //2.4のみ
  end;
  //ヘッダー
  TMyID3Header = record
    iSize:     DWORD;
    rFlag:     TMyID3HeaderFlag;
    rExtended: TMyID3HeaderExtended;
  end;

{フレーム}
  //フレームのステータスフラグ
  TMyID3FrameStatusFlag = record
    bDelFromTag:      Boolean;  //このフレームを認識できずタグを変更したときにこのフレームを削除すべきか
    bDelFromFile:     Boolean;  //このフレームを認識できずタグ以外のデータを変更したときにこのフレームを削除すべきか
    bReadOnly:        Boolean;  //読み込み専用か
    bUnknown:         Boolean;  //未知のフラグが立っているか
  end;
  //フレームのフォーマットフラグ
  TMyID3FrameFormatFlag = record
    bGroup:           Boolean;  //グループ化されているか
      iGroupID:       Byte;     //  グループ識別子
    bCompress:        Boolean;  //圧縮されているか
      iDecompSize:    DWORD;    //  圧縮伸張後のサイズ
    bEncrypt:         Boolean;  //暗号化されているか
      iEncryptMethod: Byte;     //  暗号化の方法
    bUnsync:          Boolean;  //非同期化されているか
    bDataLen:         Boolean;  //フラグが何も指定されていない場合のフレームサイズを記したデータが付加されているか
      iDataLen:       DWORD;    // フラグが何も指定されていない場合のフレームサイズ
    bUnknown:         Boolean;  //未知のフラグが立っているか
  end;

  //フレームヘッダー
  TMyID3Frame = record
    sFrameID:    String[4]; //フレームを判別するための'0'..'9','A'..'Z'で構成されている識別子
    iHeaderSize: Byte;      //ヘッダーのサイズ 2.2とそれ以降(2.3〜)で違う
    iSize:       DWORD;     //フレームサイズ
    iReadSize:   DWORD;     //正味のフレームサイズ
    rStatusFlag: TMyID3FrameStatusFlag;
    rFormatFlag: TMyID3FrameFormatFlag;
  end;


  TMyTagID3 = class(TObject)
  private
    F_sFileName: WideString;
    //Version
    F_sVersion:  WideString;
    F_iVer,
    F_iMajor,
    F_iRev:       Byte;
    //Tag
    F_sTrack,           //TRCK  トラックの番号/セット中の位置
    F_sSeries,          //TIT1  内容の属するグループの説明
    F_sTitle,           //TIT2  タイトル/曲名/内容の説明
    F_sSubTitle,        //TIT3  サブタイトル/説明の追加情報
    F_sAlbum,           //TALB  アルバム/映画/ショーのタイトル
    F_sAuthor,          //TPE1  主な演奏者/ソリスト
    F_sBand,            //TPE2  バンド/オーケストラ/伴奏
    F_sConductor,       //TPE3  指揮者/演奏者詳細情報
    F_sComposer,        //TCOM  作曲者
    F_sWriter,          //TEXT  作詞家/文書作成者
    F_sTranslator,      //TPE4  翻訳者, リミックス, その他の修正
    F_sPublisher,       //TPUB  出版社, レーベル
    F_sGenre,           //TCON  ジャンル
    F_sLength,          //TLEN  長さ
    F_sYear,            //TYER  年
    F_sDate,            //TDAT  日付
    F_sLylic,           //USLT  非同期 歌詞/文書のコピー
    F_sOriginalAlbum,   //TOAL  オリジナルのアルバム/映画/ショーのタイトル
    F_sOriginalWriter,  //TOLY  オリジナルの作詞家/文書作成者
    F_sOriginalAuthor,  //TOPE  オリジナルアーティスト/演奏者
    F_sOriginalRelease, //TORY  オリジナルのリリース年
    F_sComment: WideString//COMM

    F_rHeader: TMyID3Header;
    F_rFrame:  TMyID3Frame;

    function  F_DecodeUnsyncData(pData: PAnsiChar; iLen: DWORD): DWORD;
    function  F_GetSyncsafeSize(pData: PAnsiChar; iCount: Byte): Longword;
    function  F_GetSize(pData: PAnsiChar; iCount: Byte): Longword;
    procedure F_ReadHeader;
    function  F_GetText    (pData: PAnsiChar; iCount: DWORD): WideString;
    function  F_GetFullText(pData: PAnsiChar; iCount: DWORD): WideString;
//    function  F_GetUserText(pData: PAnsiChar; iCount: DWORD): WideString;
    procedure F_ReadFrame(pData: PAnsiChar);
    procedure F_SetFileName(sFileName: WideString);
    procedure F_Clear;
    procedure F_ReadV1;
    procedure F_ReadV2;
//    procedure F_WriteV1;
  public
    constructor Create(sFile: WideString);

    property FileName:        WideString read F_sFileName write F_SetFileName;
    property Version:         WideString read F_sVersion;
    property Major:           Byte       read F_iMajor;
    property Ver:             Byte       read F_iVer;
    property Rev:             Byte       read F_iRev;
    property Track:           WideString read F_sTrack;
    property Series:          WideString read F_sSeries;
    property Title:           WideString read F_sTitle;
    property SubTitle:        WideString read F_sSubTitle;
    property Album:           WideString read F_sAlbum;
    property Author:          WideString read F_sAuthor;
    property Band:            WideString read F_sBand;            //TPE2  バンド/オーケストラ/伴奏
    property Conductor:       WideString read F_sConductor;       //TPE3  指揮者/演奏者詳細情報
    property Composer:        WideString read F_sComposer;        //TCOM  作曲者
    property Writer:          WideString read F_sWriter;          //TEXT  作詞家/文書作成者
    property Translator:      WideString read F_sTranslator;      //TPE4  翻訳者, リミックス, その他の修正
    property Publisher:       WideString read F_sPublisher;       //TPUB  出版社, レーベル
    property Genre:           WideString read F_sGenre;
    property Length:          WideString read F_sLength;          //TLEN  長さ
    property Year:            WideString read F_sYear;            //TYER  年
    property Date:            WideString read F_sDate;            //TDAT  日付
    property Lylic:           WideString read F_sLylic;           //USLT  非同期 歌詞/文書のコピー
    property OriginalAlbum:   WideString read F_sOriginalAlbum;   //TOAL  オリジナルのアルバム/映画/ショーのタイトル
    property OriginalWriter:  WideString read F_sOriginalWriter;  //TOLY  オリジナルの作詞家/文書作成者
    property OriginalAuthor:  WideString read F_sOriginalAuthor;  //TOPE  オリジナルアーティスト/演奏者
    property OriginalRelease: WideString read F_sOriginalRelease; //TORY  オリジナルのリリース年
    property Comment:         WideString read F_sComment;
  end;


//------------------------------------------------------------------------------
implementation
uses
  SysUtils,
  Forms;


//------------------------------------------------------------------------------
//汎用ルーチン

function gfniFileRead(var pData: PAnsiChar; sFile: WideString; iOffset, iByte: DWORD): DWORD;
//ファイルからiByteバイトを読み込んでpDataにセットし、読み込んだバイト数を返す
//iOffsetはファイルの先頭からの0ベースのオフセット

var
  lh_Handle:     THandle;
  lr_Overlapped: TOverlapped;
  li_Size:       DWORD;
begin
  lh_handle := CreateFileW(
      PWideChar(sFile),      //ファイル名
      GENERIC_READ,          //アクセスモード
      FILE_SHARE_READ,       //共有モード
      nil,                  //セキュリティ
      OPEN_EXISTING,         //作成方法
      FILE_ATTRIBUTE_NORMAL, //ファイル属性
      0                      //テンプレート
  );

  try
    Result := 0;
    if (lh_Handle <> 0) then begin
      FillChar(lr_Overlapped, SizeOf(TOverlapped), 0);
      lr_Overlapped.Offset := iOffset;
      li_Size := GetFileSize(lh_Handle, nil); //4GB以上のファイルはNG
      if (iOffset < li_Size) then begin
        //オフセット分を引く
        Dec(li_Size, iOffset);
        if (iByte < li_Size) then begin
          li_Size := iByte;
        end;
        //呼び出し側でpDataのメモリーを開放をする必要あり
        pData := AllocMem(li_Size + 2); //WideString(pData)としても問題ないように
        ReadFile(lh_Handle, pData^, li_Size, Result, @lr_Overlapped);
      end;
    end;
  finally
    CloseHandle(lh_Handle);
  end;
end;

function gfniFileEndRead(var pData: PAnsiChar; sFile: WideString; iByte: DWORD): DWORD;
//ファイルの後ろからiByteバイトを読み込んでpDataにセットし、読み込んだバイト数を返す
var
  lh_Handle:     THandle;
  lr_Overlapped: TOverlapped;
  li_Offset:     DWORD;
begin
  lh_handle := CreateFileW(
      PWideChar(sFile),      //ファイル名
      GENERIC_READ,          //アクセスモード
      FILE_SHARE_READ,       //共有モード
      nil,                  //セキュリティ
      OPEN_EXISTING,         //作成方法
      FILE_ATTRIBUTE_NORMAL, //ファイル属性
      0);                    //テンプレート

  try
    Result := 0;
    if (lh_Handle <> 0) then begin
      FillChar(lr_Overlapped, SizeOf(TOverlapped), 0);
      li_Offset := GetFileSize(lh_Handle, nil) - iByte; //4GB以上のファイルはNG
      if (li_Offset > 0) then begin
        lr_Overlapped.Offset := iOffset;
        //呼び出し側でpDataのメモリーを開放をする必要あり
        pData := AllocMem(iByte + 2); //WideString(pData)としても問題ないように
        ReadFile(lh_Handle, pData^, iByte, Result, @lr_Overlapped);
      end;
    end;
  finally
    CloseHandle(lh_Handle);
  end;
end;


function gfnsByteCopy(pStr: PAnsiChar; iIndex, iCount: DWORD): AnsiString;
//pStr[iIndex]からiCount個の文字列をコピーして返す
var
  i: DWORD;
begin
  SetLength(Result, iCount);
  for i := 1 to iCount do begin
    Result[i] := pStr[iIndex + i -1];
  end;
end;


type
  TMyCharCode = (cdShift_JIS, cdUnicodeLE, cdUnicodeBE, cdUTF_16LE, cdUTF_16BE, cdUTF_8, cdUTF_8N);

function gfnsWStrBCopy(pStr: PAnsiChar; iIndex, iCount: DWORD; cdCode: TMyCharCode): WideString;
//pStr[iIndex]からiCountバイトの文字列をコピーしてWideStringにして返す
var
  i: DWORD;
  ls_Temp: AnsiString;
  lp_Buff: PAnsiChar;
begin
  if  (pStr[iIndex]    = #$EF)
  and (pStr[iIndex +1] = #$BB)
  and (pStr[iIndex +2] = #$BF)
  then begin
    //UTF-8 BOMあり
    cdCode := cdUTF_8;
    Inc(iIndex, 3);
    Dec(iCount, 3);
  end else
  if  (pStr[iIndex]    = #$FF)
  and (pStr[iIndex +1] = #$FE)
  then begin
    //UTF-16 BOMありのリトルエンディアン
    cdCode := cdUnicodeLE;
    Inc(iIndex, 2);
    Dec(iCount, 2);
  end else
  if  (pStr[iIndex]    = #$FE)
  and (pStr[iIndex +1] = #$FF)
  then begin
    //UTF-16 BOMありのビッグエンディアン
    cdCode := cdUnicodeBE;
    Inc(iIndex, 2);
    Dec(iCount, 2);
  end;

  if (cdCode = cdUTF_16BE) or (cdCode = cdUnicodeBE) then begin
    //UTF-16 ビッグエンディアン
    lp_Buff := AllocMem(iCount + 2);
    try
      for i := 0 to iCount -1 do begin
        if (i mod 2 = 0) then begin
          lp_Buff[i]     := pStr[iIndex + i +1];
          lp_Buff[i + 1] := pStr[iIndex + i];
        end;
      end;
      Result := WideString(PWideChar(lp_Buff));
    finally
      FreeMem(lp_Buff);
    end;
  end else begin
    ls_Temp := gfnsByteCopy(pStr, iIndex, iCount);
    if (cdCode = cdShift_JIS) then begin
      //Shift-JIS
      Result := AnsiString(ls_Temp);
    end else if (cdCode = cdUTF_8) or (cdCode = cdUTF_8N) then begin
      //UTF-8
      Result := UTF8Decode(ls_Temp);
    end else if (cdCode = cdUnicodeLE) or (cdCode = cdUTF_16LE) then begin
      //UTF-16 リトルエンディアン
      Result := WideString(PWideChar(PChar(ls_Temp + #0#0)));
    end;
  end;
end;

//汎用ルーチン終わり


//==============================================================================
const
  lciTAG_HEADERSIZE = 10;

//------------------------------------------------------------------------------
constructor TMyTagID3.Create(sFile: WideString);
begin
  inherited Create;

  F_SetFileName(sFile);
end;

procedure TMyTagID3.F_Clear;
begin
  F_sVersion   := '';
  F_iVer       := 0;
  F_iMajor     := 0;
  F_iRev       := 0;

  F_sTrack           := '';  //TRCK  トラックの番号/セット中の位置
  F_sSeries          := '';  //TIT1  内容の属するグループの説明
  F_sTitle           := '';  //TIT2  タイトル/曲名/内容の説明
  F_sSubTitle        := '';  //TIT3  サブタイトル/説明の追加情報
  F_sAlbum           := '';  //TALB  アルバム/映画/ショーのタイトル
  F_sAuthor          := '';  //TPE1  主な演奏者/ソリスト
  F_sBand            := '';  //TPE2  バンド/オーケストラ/伴奏
  F_sConductor       := '';  //TPE3  指揮者/演奏者詳細情報
  F_sComposer        := '';  //TCOM  作曲者
  F_sWriter          := '';  //TEXT  作詞家/文書作成者
  F_sTranslator      := '';  //TPE4  翻訳者, リミックス, その他の修正
  F_sPublisher       := '';  //TPUB  出版社, レーベル
  F_sGenre           := '';  //TCON  ジャンル
  F_sLength          := '';  //TLEN  長さ
  F_sYear            := '';  //TYER  年
  F_sDate            := '';  //TDAT  日付
  F_sLylic           := '';  //USLT  非同期 歌詞/文書のコピー
  F_sOriginalAlbum   := '';  //TOAL  オリジナルのアルバム/映画/ショーのタイトル
  F_sOriginalWriter  := '';  //TOLY  オリジナルの作詞家/文書作成者
  F_sOriginalAuthor  := '';  //TOPE  オリジナルアーティスト/演奏者
  F_sOriginalRelease := '';  //TORY  オリジナルのリリース年
  F_sComment         := '';  //COMM  コメント
end;

function TMyTagID3.F_DecodeUnsyncData(pData: PAnsiChar; iLen: DWORD): DWORD;
{
非同期化処理されたデータを元に戻し、元のサイズを返す
ヘッダーのフラグで非同期フラグが立っている場合、ヘッダーのデータ中に同期信号と同
じパターンが現れるのを避けるための処理が行われているのでそれを元に戻す。

回避処理の内容は、同期信号のパターンが2進表記で
%11111111 111xxxxx
となるので xFF の後に x00 を挿入して
%11111111 00000000 111xxxxx
とする。これで同期信号と同じパターンを回避する。
回避処理の内容は頭からデータを1バイトづつ見ていき xFF に出くわし、その後の1バイト
が xE0 以上または x00 なら x00 を加える。または単純に xFF の後に x00 を加える。

このルーチンではその逆を行うため xFF の次に現れる x00 を取り除く処理を行う。
この結果ヘッダーの実際のサイズは取り除いた x00 の分だけ減る。
}

var
  i, li_Shift: DWORD;
  lp_Pos: PAnsiChar;
begin
  lp_Pos := pData;
  li_Shift := 0;

  for i := 0 to iLen - (1 + li_Shift) do begin
    if  (i > 0)
    and (lp_Pos[-1] = #$FF)
    and (lp_Pos[0]  = #$0)
    then begin
      Inc(li_Shift);
      Inc(lp_Pos);
      lp_Pos[-li_Shift] := lp_Pos[0];
    end else if (li_Shift > 0) then begin
      lp_Pos[-li_Shift] := lp_Pos[0];
    end;
    Inc(lp_Pos);
  end;
  Result := iLen - li_Shift;
end;

function lfniPower(iBase, iExponent: WORD): Longword;
var
  i: Integer;
begin
  if (iExponent = 0) then begin
    Result := 1;
  end else begin
    Result := iBase;
    for i := 1 to iExponent - 1 do begin
      Result := Result * iBase;
    end;
  end;
end;

function TMyTagID3.F_GetSyncsafeSize(pData: PAnsiChar; iCount: Byte): Longword;
{同期信号である %11111111 111xxxxx と同じパターンになるのを防ぐために最上位ビット
が常に0であるような表現の数値として返す。
%11111111 は16進ならxFF、10進なら255。
%01111111 は16進ならx7F、10進なら127。
通常1バイトは255(xFF)が最大値となるが上記の場合は127(x7F)が最大値であり128(x80)に
なると桁あがりする。
よってx100は通常なら256だが上記のような場合は128となる。
x10000は通常なら65536(256*256)だが16384(128*128)となる。
}

var
  i: Integer;
begin
  Result := 0;
  for i := 0 to iCount -1 do begin
    Inc(Result, Ord(pData[(iCount - i) -1]) * lfniPower($80, i));
  end;
{
↑は
  Result :=
    Ord(pData[0]) * $200000 + //128 * 128 * 128
    Ord(pData[1]) * $4000 +   //128 * 128
    Ord(pData[2]) * $80 +     //128
    Ord(pData[3]);
というようなことを汎用的にやっている
}

end;

function TMyTagID3.F_GetSize(pData: PAnsiChar; iCount: Byte): Longword;
//↑と違い通常の数値として返す。
var
  i: Integer;
begin
  Result := 0;
  for i := 0 to iCount -1 do begin
    Inc(Result, Ord(pData[(iCount - i) -1]) * lfniPower($100, i));
  end;
end;

function TMyTagID3.F_GetText(pData: PAnsiChar; iCount: DWORD): WideString;
//改行を含まないテキストを返す
var
  li_Index, li_Enc: DWORD;
begin
  li_Enc := Ord(pData[0]);
  li_Index := 1; //文字エンコードを飛ばす
  Dec(iCount);   //文字エンコード(1Byte)分を減らす
  if (li_Enc = $0) then begin
    Result := gfnsByteCopy(pData, li_Index, iCount);
  end else if (li_Enc = $1) then begin
    //BOMありのUTF-16
    //とりあえずリトルエンディアンを指定しているが関数内でBOMを自動判定している
    Result := gfnsWStrBCopy(pData, li_Index, iCount, cdUnicodeLE);
  end else if (li_Enc = $2) then begin
    //BOMなしのビッグエンディアン
    Result := gfnsWStrBCopy(pData, li_Index, iCount, cdUTF_16BE);
  end else if (li_Enc = $3) then begin
    //UTF-8
    Result := UTF8Decode(gfnsByteCopy(pData, li_Index, iCount));
  end;
end;

function TMyTagID3.F_GetFullText(pData: PAnsiChar; iCount: DWORD): WideString;
//USLT,COMMなどの改行を含むテキストを返す
var
  i, li_Enc, li_Index: DWORD;
begin
  Result := '';

  li_Enc := Ord(pData[0]);
  li_Index := (1 + 3);  //文字エンコード(1Byte)と言語コード(3Byte)を読み飛ばす
  Dec(iCount, (1 + 3));
  if (li_Enc = $0)  //ISO-8859-1
  or (li_Enc = $3)  //UTF-8
  then begin
    if (iCount > 0) then begin
      //Content decriptorを読み飛ばす
      for i := 0 to iCount -1 do begin
        if (pData[li_Index + i] = #0) then begin
          Dec(iCount,   (i + 1));
          Inc(li_Index, (i + 1));
          Break;
        end;
      end;

      Result := gfnsByteCopy(pData, li_Index, iCount);
      //必要なら説明文を頭に加える
      //Result := gfnsByteCopy(pData, 0, li_Index - 2) + #13 + Result;
      if (li_Enc = $3) then begin
        //UTF-8
        Result := UTF8Decode(Result);
      end;
    end;
  end else begin
    //Unicode
    //Content decriptorを読み飛ばす
    for i := 0 to iCount -1 do begin  //iCountは文字数ではなくバイト数
      if  (i mod 2 = 1)  //Unicodeなので2バイトごとの処理
      and (pData[li_Index + (i -1)] = #0) //終了文字
      and (pData[li_Index + i]      = #0) //2つで終了
      then begin
        Dec(iCount,   (i + 1));
        Inc(li_Index, (i + 1));
        Break;
      end;
    end;

    if (iCount > 0) then begin
      if (li_Enc = $1) then begin
        //BOMありのUTF-16
        Result := gfnsWStrBCopy(pData, li_Index, iCount, cdUnicodeLE);
      end else begin
        //BOMなしのビッグエンディアン
        Result := gfnsWStrBCopy(pData, li_Index, iCount, cdUTF_16BE);
      end;
    end;
  end;
end;

procedure TMyTagID3.F_ReadHeader;
var
  lp_Buff: PAnsiChar;
  li_Flag: Integer;
  li_Size: DWORD;
begin
{
ID3v2ヘッダ 10バイト固定
オフセット長さ 内容
0         3    ID3 の識別文字3文字
3         2    バージョン
5         1    フラグ
6         4    サイズ(Syncsafe)
}

  FillChar(F_rHeader, SizeOf(F_rHeader), 0);
  li_Size := gfniFileRead(lp_Buff, F_sFileName, lciTAG_HEADERSIZE);  //10バイト固定
  try
    if (li_Size = lciTAG_HEADERSIZE) then begin
      if  (lp_Buff[0] = 'I')
      and (lp_Buff[1] = 'D')
      and (lp_Buff[2] = '3')
      then begin
        F_iVer   := 2;  //最初の3バイトが'ID3'ならID3v2
        F_iMajor := Ord(lp_Buff[3]);
        F_iRev   := Ord(lp_Buff[4]);
        li_Flag  := Ord(lp_Buff[5]);
        F_rHeader.iSize := F_GetSyncsafeSize(@lp_Buff[6], 4);
        //サイズにはヘッダー自身の10バイトとフッターの10バイトは含まない

        F_rHeader.rFlag.bUnsync := ByteBool(li_Flag and $80);         //7bit 非同期化処理されているか
        if (F_iMajor = 2) then begin
          F_rHeader.rFlag.bCompress := ByteBool(li_Flag and $40);     //6bit 2.2のみ 圧縮
        end else if (F_iMajor >= 3) then begin
          F_rHeader.rFlag.bExtended     := ByteBool(li_Flag and $40); //6bit 2.3〜 拡張ヘッダーがあるか
          F_rHeader.rFlag.bExperimental := ByteBool(li_Flag and $20); //5bit 2.3〜 テスト用か
          if (F_iMajor >= 4) then begin
            F_rHeader.rFlag.bFooter := ByteBool(li_Flag and $10);     //4bit 2.4 フッターがあるか
          end;
        end;
      end;
    end;
  finally
    FreeMem(lp_Buff);
  end;
end;

procedure TMyTagID3.F_ReadFrame(pData: PAnsiChar);
  function lfni_GetSize(pFrameData: PAnsiChar; iByte: Byte): Integer;
  begin
    Result := F_GetSize(@pFrameData[F_rFrame.iHeaderSize], iByte);
    Inc(F_rFrame.iHeaderSize, iByte);
    Dec(F_rFrame.iSize,       iByte);
  end;
begin
  FillChar(F_rFrame, SizeOf(TMyID3Frame), 0);

  //フレームIDが正しくない場合はフレームヘッダーのサイズは0のまま
  if (pData[0] in ['0'..'9', 'A'..'Z']) then begin
    if (F_iMajor = 2) then begin
      //Ver2.2
      F_rFrame.sFrameID := gfnsByteCopy(pData, 0, 3);
      //ID、サイズとも3バイトでフラグのヘッダーサイズは6バイト
      F_rFrame.iHeaderSize := 3 + 3;
      F_rFrame.iSize       := F_GetSize(@pData[3], 3);
      F_rFrame.iReadSize   := F_rFrame.iSize;
    end else if (F_iMajor >= 3) then begin
      //Ver2.3以上
      F_rFrame.sFrameID := gfnsByteCopy(pData, 0, 4);
      //ID、サイズとも4バイト、2バイトのフラグでフラグのヘッダーサイズは10バイト
      F_rFrame.iHeaderSize := 4 + 4 + 2;
      if (F_iMajor = 3) then begin
        //Ver2.3
        F_rFrame.iSize := F_GetSize(@pData[4], 4);
        F_rFrame.rStatusFlag.bDelFromTag  := ByteBool(Ord(pData[8]) and $80);  //タグが変更されたらこのフレームを削除すべきか
        F_rFrame.rStatusFlag.bDelFromFile := ByteBool(Ord(pData[8]) and $40);  //本体データの一部が変更されたらこのフレームを削除すべきか(データが完全に置き換えられた場合は関係ない)
        F_rFrame.rStatusFlag.bReadOnly    := ByteBool(Ord(pData[8]) and $20);  //読み取り専用か
        //未知のフラグが立っていたらクリアしない限り変更してはいけない
        F_rFrame.rStatusFlag.bUnknown     := ByteBool(Ord(pData[8]) and $10)
                                          or ByteBool(Ord(pData[8]) and $08)
                                          or ByteBool(Ord(pData[8]) and $04)
                                          or ByteBool(Ord(pData[8]) and $02)
                                          or ByteBool(Ord(pData[8]) and $01); //未知のフラグが立っているか

        F_rFrame.rFormatFlag.bCompress    := ByteBool(Ord(pData[9]) and $80);  //圧縮されているか
        F_rFrame.rFormatFlag.bEncrypt     := ByteBool(Ord(pData[9]) and $40);  //暗号化されているか
        F_rFrame.rFormatFlag.bGroup       := ByteBool(Ord(pData[9]) and $20);  //グループ化されているか
        F_rFrame.rFormatFlag.bUnknown     := ByteBool(Ord(pData[9]) and $10)  //未知のフラグが立っているか
                                          or ByteBool(Ord(pData[9]) and $08)
                                          or ByteBool(Ord(pData[9]) and $04)
                                          or ByteBool(Ord(pData[9]) and $02)
                                          or ByteBool(Ord(pData[9]) and $01);
        //以降の順番は変えてはいけない
        if (F_rFrame.rFormatFlag.bCompress) then F_rFrame.rFormatFlag.iDecompSize    := lfni_GetSize(pData, 4);
        if (F_rFrame.rFormatFlag.bEncrypt)  then F_rFrame.rFormatFlag.iEncryptMethod := lfni_GetSize(pData, 1);
        if (F_rFrame.rFormatFlag.bGroup)    then F_rFrame.rFormatFlag.iGroupID       := lfni_GetSize(pData, 1);
        F_rFrame.iReadSize := F_rFrame.iSize;
      end else begin
        //Ver2.4以上
        F_rFrame.iSize := F_GetSyncsafeSize(@pData[4], 4);
        F_rFrame.rStatusFlag.bDelFromTag  := ByteBool(Ord(pData[8]) and $40); //タグ変更でこのフレームを削除するか
        F_rFrame.rStatusFlag.bDelFromFile := ByteBool(Ord(pData[8]) and $20); //本体データの変更でこのフレームを削除するか
        F_rFrame.rStatusFlag.bReadOnly    := ByteBool(Ord(pData[8]) and $10); //読み取り専用か
        F_rFrame.rStatusFlag.bUnknown     := ByteBool(Ord(pData[8]) and $80)  //未知のフラグが立っているか
                                          or ByteBool(Ord(pData[8]) and $08)
                                          or ByteBool(Ord(pData[8]) and $04)
                                          or ByteBool(Ord(pData[8]) and $02)
                                          or ByteBool(Ord(pData[8]) and $01);

        F_rFrame.rFormatFlag.bGroup       := ByteBool(Ord(pData[9]) and $40); //グループ化
        F_rFrame.rFormatFlag.bCompress    := ByteBool(Ord(pData[9]) and $8);  //圧縮(v2.3と違いフレームヘッダの後に圧縮前のサイズは追加されていない)
        F_rFrame.rFormatFlag.bEncrypt     := ByteBool(Ord(pData[9]) and $4);  //暗号化
        F_rFrame.rFormatFlag.bUnsync      := ByteBool(Ord(pData[9]) and $2);  //非同期化処理
        F_rFrame.rFormatFlag.bDataLen     := ByteBool(Ord(pData[9]) and $1);  //データ長指定子
        F_rFrame.rFormatFlag.bUnknown     := ByteBool(Ord(pData[9]) and $80)  //未知のフラグが立っているか
                                          or ByteBool(Ord(pData[9]) and $20)
                                          or ByteBool(Ord(pData[9]) and $10);

        //最初に非同期化処理解除行う
        if (F_rFrame.rFormatFlag.bUnsync) then begin
          F_rFrame.iReadSize := F_DecodeUnsyncData(@pData[F_rFrame.iHeaderSize], F_rFrame.iSize);
        end else begin
          F_rFrame.iReadSize := F_rFrame.iSize;
        end;
        //次に暗号化を解く
        if (F_rFrame.rFormatFlag.bEncrypt) then begin
          //暗号化を解く。。のはまぁ無理なのでこのフラグが立っていたら次のフレームに飛ばすようにした方が良いのかも
        end;
        //最後に圧縮解除
        if (F_rFrame.rFormatFlag.bCompress) then begin
          //圧縮解除
        end;

        //以降の順番は変えてはいけない
        if (F_rFrame.rFormatFlag.bGroup)   then F_rFrame.rFormatFlag.iGroupID       := lfni_GetSize(pData, 1); //グループ化
        if (F_rFrame.rFormatFlag.bEncrypt) then F_rFrame.rFormatFlag.iEncryptMethod := lfni_GetSize(pData, 1); //暗号化
        if (F_rFrame.rFormatFlag.bDataLen) then F_rFrame.rFormatFlag.iDataLen       := lfni_GetSize(pData, 4); //データ長指定子
      end;
    end;
  end;
end;

procedure TMyTagID3.F_SetFileName(sFileName: WideString);
begin
  F_sFileName := sFileName;
  F_Clear;

  //ID3v2としてヘッダーを読んでみる
  F_ReadHeader;
  if (F_iVer = 2) then begin
    //ID3v2
    F_iVer := 2;
    F_sVersion := WideFormat('%d.%d.%d', [F_iVer, F_iMajor, F_iRev]);
    F_ReadV2;
  end else begin
    //ID3v2ではないのでID3v1として読んでみる
    F_ReadV1;
    if (F_iVer = 1) then begin
      //ID3v1だった
      F_sVersion := WideFormat('%d.%d', [F_iVer, F_iMajor]);
    end;
  end;
end;


procedure TMyTagID3.F_ReadV2;
var
  lp_Buff: PAnsiChar;
  li_Size, li_Offset: DWORD;
  li_SeekLen, li_SeekPos, li_SeekCount: DWORD;
  ls_Text: WideString;
begin
  if (F_rHeader.iSize > 0) then begin
    li_Size := gfniFileRead(lp_Buff, F_sFileName, lciTAG_HEADERSIZE, F_rHeader.iSize); //10バイト目から読み込む
    try
      if (li_Size = F_rHeader.iSize) then begin
        li_Offset := 0;
        if (F_rHeader.rFlag.bUnsync) then begin
          //タグ全体に非同期化処理を行ってあるので元に戻し、戻した後のサイズを返す
          F_rHeader.iSize := F_DecodeUnSyncData(lp_Buff, li_Size);
        end;
        if (F_rHeader.rFlag.bExtended) then begin
          //拡張ヘッダー
          F_rHeader.rExtended.iSize := F_GetSize(lp_Buff, 4);
          if (F_iMajor = 3) then begin
            //2.3
            F_rHeader.rExtended.iPaddingSize := F_GetSize(@lp_Buff[6], 4);
            F_rHeader.rExtended.bCrc         := ByteBool(Ord(lp_Buff[4]) and $80);
            if (F_rHeader.rExtended.bCrc) then begin
              F_rHeader.rExtended.iCrcData := F_GetSize(@lp_Buff[10], 4);
            end;
            Dec(F_rHeader.iSize, F_rHeader.rExtended.iPaddingSize);  //パディング分を引いておく
            {
            拡張ヘッダーサイズ(rExtended.iSize)には拡張ヘッダーサイズを表す4バイトは含まれない。
            そのためrExtended.iSizeは6か10のどちらかとなる。
            6というのはCRCフラグがオフの場合でパディングサイズを表す4バイトとフラグの2バイト。
            10というのはCRCフラグがオンで上記の6バイトにCRCデータの4バイト。
            で、結局フレームを読むためのオフセットはヘッダーサイズを表した4バイト + rExtended.iSize となる。
            }

            li_Offset := 4 + F_rHeader.rExtended.iSize;
          end else if (F_iMajor = 4) then begin
            //2.4
            //2.4は2.3と違い拡張ヘッダーサイズを表す4バイトも含んだサイズ。
            //なのでオフセットは単純にrExtended.iSizeを足すだけ。
            li_Offset := F_rHeader.rExtended.iSize;
            //拡張ヘッダーの詳細は読み込みには必要ないので割愛
          end;
        end;

        repeat
          F_ReadFrame(@lp_Buff[li_Offset]);
          Inc(li_Offset, F_rFrame.iHeaderSize);
          //暗号化されていたら(復元できないので)処理に入らない
          if (F_rFrame.iSize > 0) and not(F_rFrame.rFormatFlag.bEncrypt) then begin
            if (F_rFrame.sFrameID = 'COMM') or (F_rFrame.sFrameID = 'COM') then begin
              //コメント
              //複数のコメントタグがあったら追加する
              F_sComment := F_sComment + #13#13 + F_GetFullText(@lp_Buff[li_Offset], F_rFrame.iReadSize);
            end else if (F_rFrame.sFrameID = 'USLT') or (F_rFrame.sFrameID = 'ULT') then begin
              //歌詞
              //複数の歌詞タグがあったら追加する
              F_sLylic := F_sLylic + #13#13 + F_GetFullText(@lp_Buff[li_Offset], F_rFrame.iReadSize);
//            end else if (F_rFrame.sFrameID = 'TXXX') or (F_rFrame.sFrameID = 'TXX') then begin
//              //ユーザー定義テキスト(これのみ複数行テキスト)
//              //必要ならコードを書く
//              F_sUserText := F_sUserText + #13#13 + F_GetUserText(@lp_Buff[li_Offset], F_rFrame.iReadSize);
//            end else if (F_rFrame.sFrameID = 'SEEK') then begin
//※要検証
//              //タグ分割
//              li_SeekLen   := F_GetSize(@lp_Buff[li_Offset], 4);            //分割タグまでの距離
//              li_SeekCount := F_rHeader.iSize - li_Offset - F_rFrame.iSize; //分割の残りのバイト数
//              li_SeekPos   := F_rHeader.iSize - li_SeekCount + li_SeekLen;  //分割タグのインデックス
//
//              //until〜 の条件に合致するように調整
//              //ヘッダーのサイズから今まで読み込んだ分と、このSEEKフレーム分を差し引く。
//              Dec(F_rHeader.iSize, (li_Offset + F_rFrame.iSize));
//              li_Offset := 0;
//
//              //残りを読み込みなおす
//              FreeMem(lp_Buff);
//              gfniFileRead(lp_Buff, li_SeekPos, li_SeekCount);
//
//              Continue;
            end else if (F_rFrame.sFrameID[1] = 'T') then begin
              //改行無しのテキスト
              ls_Text := F_GetText(@lp_Buff[li_Offset], F_rFrame.iReadSize);

              if (F_rFrame.sFrameID = 'TRCK') or (F_rFrame.sFrameID = 'TRK') then begin
                //トラックの番号/セット中の位置
                F_sTrack := ls_Text;
              end else if (F_rFrame.sFrameID = 'TIT1') or (F_rFrame.sFrameID = 'TT1') then begin
                //内容の属するグループの説明
                F_sSeries := ls_Text;
              end else if (F_rFrame.sFrameID = 'TIT2') or (F_rFrame.sFrameID = 'TT2') then begin
                //タイトル/曲名/内容の説明
                F_sTitle := ls_Text;
              end else if (F_rFrame.sFrameID = 'TIT3') or (F_rFrame.sFrameID = 'TT3') then begin
                //サブタイトル/説明の追加情報
                F_sSubTitle := ls_Text;
              end else if (F_rFrame.sFrameID = 'TALB') or (F_rFrame.sFrameID = 'TAL') then begin
                //アルバム/映画/ショーのタイトル
                F_sAlbum := ls_Text;
              end else if (F_rFrame.sFrameID = 'TPE1') or (F_rFrame.sFrameID = 'TP1') then begin
                //主な演奏者/ソリスト
                F_sAuthor := ls_Text;
              end else if (F_rFrame.sFrameID = 'TPE2') or (F_rFrame.sFrameID = 'TP2') then begin
                //バンド/オーケストラ/伴奏
                F_sBand := ls_Text;
              end else if (F_rFrame.sFrameID = 'TPE3') or (F_rFrame.sFrameID = 'TP3') then begin
                //指揮者/演奏者詳細情報
                F_sConductor := ls_Text;
              end else if (F_rFrame.sFrameID = 'TCOM') or (F_rFrame.sFrameID = 'TCM') then begin
                //作曲者
                F_sComposer := ls_Text;
              end else if (F_rFrame.sFrameID = 'TEXT') or (F_rFrame.sFrameID = 'TXT') then begin
                //作詞家/文書作成者
                F_sWriter := ls_Text;
              end else if (F_rFrame.sFrameID = 'TPE4') or (F_rFrame.sFrameID = 'TP4') then begin
                //翻訳者, リミックス, その他の修正
                F_sTranslator := ls_Text;
              end else if (F_rFrame.sFrameID = 'TPUB') or (F_rFrame.sFrameID = 'TPB') then begin
                //出版社, レーベル
                F_sPublisher := ls_Text;
              end else if (F_rFrame.sFrameID = 'TCON') or (F_rFrame.sFrameID = 'TCN') then begin
                //ジャンル
                F_sGenre := ls_Text;
              end else if (F_rFrame.sFrameID = 'TLEN') or (F_rFrame.sFrameID = 'TLE') then begin
                //長さ
                F_sLength := ls_Text;
              end else if (F_rFrame.sFrameID = 'TYER') or (F_rFrame.sFrameID = 'TYE') then begin
                //年
                F_sYear := ls_Text;
              end else if (F_rFrame.sFrameID = 'TDAT') or (F_rFrame.sFrameID = 'TDA') then begin
                //日付
                F_sDate := ls_Text;
              end else if (F_rFrame.sFrameID = 'TOAL') or (F_rFrame.sFrameID = 'TOT') then begin
                //オリジナルのアルバム/映画/ショーのタイトル
                F_sOriginalAlbum := ls_Text;
              end else if (F_rFrame.sFrameID = 'TOLY') or (F_rFrame.sFrameID = 'TOL') then begin
                //オリジナルの作詞家/文書作成者
                F_sOriginalWriter := ls_Text;
              end else if (F_rFrame.sFrameID = 'TOPE') or (F_rFrame.sFrameID = 'TOA') then begin
                //オリジナルアーティスト/演奏者
                F_sOriginalAuthor := ls_Text;
              end else if (F_rFrame.sFrameID = 'TORY') or (F_rFrame.sFrameID = 'TOR') then begin
                //オリジナルのリリース年
                F_sOriginalRelease := ls_Text;
              end;
            end;
            //F_rFrame.iSizeにはフレーム個別の非同期化解除前のサイズが入っている。
            //次のフレームの開始位置を得るにはF_rFrame.rFlag.iUnsyncLengthではなくこっちを用いる
            Inc(li_Offset, F_rFrame.iSize);
          end;
          Application.ProcessMessages;
        until (li_Offset >= F_rHeader.iSize) or (F_rFrame.iHeaderSize = 0);
      end;
    finally
      FreeMem(lp_Buff);
    end;
  end;
end;

procedure TMyTagID3.F_ReadV1;
{2008-04-02:
ID3タグVer1を取得
http://ja.wikipedia.org/wiki/ID3タグ
}

var
  lp_Buff: PAnsiChar;
  li_Size: DWORD;
begin
  li_Size := gfniFileEndRead(lp_Buff, F_sFileName, 128);
  try
    if (li_Size = 128) then begin
      if  (lp_Buff[0] = 'T')
      and (lp_Buff[1] = 'A')
      and (lp_Buff[2] = 'G')
      then begin
        F_iVer := 1;
        F_sTitle   := gfnsByteCopy(lp_Buff,  3, 30);  //曲名
        F_sAuthor  := gfnsByteCopy(lp_Buff, 33, 30);  //アーティスト
        F_sAlbum   := gfnsByteCopy(lp_Buff, 63, 30);  //アルバム
        F_sYear    := gfnsByteCopy(lp_Buff, 93,  4);  //日付
        F_sComment := gfnsByteCopy(lp_Buff, 97, 30);  //コメント
        F_sGenre   := IntToStr(Ord(lp_Buff[127]));    //ジャンル
        if (lp_Buff[125] = #0) and (lp_Buff[126] <> #0) then begin
          F_sTrack := IntToStr(Ord(lp_Buff[126]));    //トラック
          F_iMajor := 1;
        end;
      end;
    end;
  finally
    FreeMem(lp_Buff);
  end;
end;

2008-12-20