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

ID3タグ自作関数 ID3v2簡易版

ID3v2はv1と違って複雑な仕様になっています。
おまけにバージョンごとに細かな仕様が異なっていたり変わってしまっていたりします。
それらにいちいち対応させないといけないのがなんとも頭の痛いところなのですが、そういう細かい仕様には目をつむり重要なものだけに絞り込んだ割り切った実装とすればそれほど難しくはなくなります。
実際それで特に困ることも少ないと思いますし。

細かい仕様にも対応したい場合はまじめにID3v2読み込みも合わせてどうぞ。


参考サイト


はじめに

簡単に言うとID3v2は、まず10バイトのタグヘッダーがあり、そのタグヘッダーにバージョンとタグ全体のサイズが書かれています。
そしてその後にタイトルやアーティストなどのタグがそれぞれフレームという構造に入れられ格納されています。

タグヘッダー(10バイト固定)
フレーム(タイトル)
フレーム(アーティスト)
フレーム(アルバム)

mp3本体

おおまかにはこんな感じで。
フレームの並びは任意で、しかもあってもなくてもOKという柔軟な仕様になっています。
そのフレームにはフレームヘッダーがあり、フレームがどのタグを格納しているかを示す識別子やそのフレームのサイズなどが記されています。
フレームヘッダーの後にはそのサイズ分のフレームデータがありデータの後には次のフレームの頭があります。
つまり順繰りに次のフレーム、次のフレームと芋づる式に読んでいけるわけです。
そうやってどんどんフレームを読んでいって、最終的にフレームがなくなればそこでタグ終了となります。

タグヘッダー

まずはタグヘッダーの読み込みと解析から。

ID3v2ヘッダ 10バイト固定
オフセット 長さ(バイト) 内容
0 3 'ID3' の識別文字3文字
3 2 バージョン
5 1 フラグ
6 4 サイズ(Syncsafe)

v2のタグヘッダーは10バイト固定でファイルの先頭に置かれます。

const
  lciTAG_HEADERSIZE = 10;  //ヘッダーのサイズは10バイト

procedure TMyTagID3.F_ReadHeader;
var
  lp_Buff: PAnsiChar;
  li_Size: DWORD;
begin
  li_Size := gfniFileRead(lp_Buff, F_sFileName, 0, 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'ならv2
        F_iMajor   := Ord(lp_Buff[3]);
        F_iRev     := Ord(lp_Buff[4]);
        F_iTagSize := F_GetSyncsafeSize(@lp_Buff[6], 4); //サイズは4バイト
        //このサイズにはヘッダー自身の10バイトは含まない
      end;
    end;
  finally
    FreeMem(lp_Buff);
  end;
end;

Synchsafe整数

タグヘッダーに記されている4バイトのサイズは同期信号パターンを回避するように工夫されたやり方で書かれています。
ちょっとややこしいのですが、要は1バイトで表す最大値を8ビットフルに使うのではなく7ビット分しか使わないやり方です。

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 と同じパターンになるのを防ぐために %11111111(xFF)
を作らないよう最上位ビットが常に0であるような表現の数値を通常の数値に戻して返す。
%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;

同期信号というのは(詳しくないので良く分からないのですが)MPEGフォーマットに必要な信号のようで、そのパターンは2進値で1が11個連続しているもののようです。
つまり

%11111111 111xxxxx

というパターン (xは1でも0でもどっちでも良い)で、これと同じパターンがタグ中に含まれるとID3v2に対応していない機器で誤動作してしまう恐れがあると。
それを避けるために最上位ビット(左端のビット)を1にしなければ良いと。
そうすれば同期信号と同じパターンになってしまうことはないと。
そういうことで1バイトを %01111111 を最大値としてそれより大きい場合は桁上げしてしまうというやり方で数値表現してしまいましょうと。
そういうことのようです。

例えば127を2進値で表すと01111111になります。
128は11111111になります。
このままだと最上位ビットが1になってしまうので128は桁上げして00000001 00000000としてしまいます。

125は 00000000 01111101
126は 00000000 01111110
127は 00000000 01111111
128は 00000001 00000000
129は 00000001 00000001
130は 00000001 00000010
131は 00000001 00000011

  ...

253は 00000001 01111101
254は 00000001 01111110
255は 00000001 01111111
256は 00000002 00000000

というようになります。


フレームヘッダー

タイトルやアーティスト名などのタグはID3v2ではフレームという(レコード型のような)もので扱われます。
そのフレームの最初には、ID、サイズ、フラグを記したフレームヘッダーがあります。
フレームヘッダーのサイズはv2.2が6バイト、v2.3以降が10バイトです。

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_GetSize(pData: PAnsiChar; iCount: Byte): Longword;
//F_GetSyncsafeSizeと違い通常の数値として返す。
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;
{
↑は
  Result :=
    Ord(pData[0]) * $1000000 +  //256 * 256 * 256
    Ord(pData[1]) * $10000   +  //256 * 256
    Ord(pData[2]) * $100     +  //256
    Ord(pData[3]);
というようなことを汎用的にやっている
}

end;

非同期化処理されていない数値とは、ヘッダーの項で述べたF_GetSyncsafeのような特殊なことをしていない普通の数値のことです。
ただバイトの並びが普通に数値をファイルへ保存した場合と逆になるので上のような処理が必要になります。

procedure TMyTagID3.F_ReadFrame(pData: PAnsiChar);
begin
  F_iFrameHeaderSize := 0;
  //フレームIDが正しくない場合はフレームヘッダーのサイズは0のまま
  if (pData[0] in ['0'..'9', 'A'..'Z']) then begin
    if (F_iMajor = 2) then begin
      //Ver2.2
      F_sFrameID := gfnsByteCopy(pData, 0, 3);
      F_iFrameHeaderSize := 3 + 3;
      //ID、サイズとも3バイトでフレームのヘッダーサイズは6バイト
      F_iFrameSize := F_GetSize(@pData[3], 3);
    end else if (F_iMajor >= 3) then begin
      //Ver2.3以上
      //ID、サイズとも4バイトで2バイトのフラグでフレームのヘッダーサイズは10バイト
      F_iFrameHeaderSize := 4 + 4 + 2;
      F_sFrameID := gfnsByteCopy(pData, 0, 4);
      if (F_iMajor = 3) then begin
        //Ver2.3
        F_iFrameSize := F_GetSize(@pData[4], 4);
      end else begin
        //Ver2.4以上
        F_iFrameSize := F_GetSyncsafeSize(@pData[4], 4);  //サイズは4バイトのフレームIDの次の4バイトでSyncsafe
      end;
    end;
  end;
end;

フレームIDの先頭が'0'..'9'、'A'..'Z'でない場合はフレームではないと判定していますがもしかしたら'A'..'Z'だけで判定して良いのかも知れません。
ただ仕様書にはフレームIDの先頭には数字は不可とかそういったことは書かれていないようなので判定条件に入れています。


フレーム本体

F_ReadHeaderでタグ全体のサイズをF_iTagSizeにセットしました。
これを使いタグヘッダーの10バイトをスキップしてからF_iTagSize分データを読み込みます。
この読み込んだデータはフレームが積み重なった形のデータの塊であるとも言え、頭から順にF_ReadFrameでフレーム単位で読み込んでいきます。

procedure TMyTagID3.F_ReadV2;
var
  lp_Buff: PAnsiChar;
  li_Size, li_Offset: DWORD;
  ls_Text: WideString;
begin
  if (F_iTagSize > 0) then begin
    li_Offset := 0;
    //タグヘッダーの10バイトをスキップして読み込む
    li_Size := gfniFileRead(lp_Buff, F_sFileName, lciTAG_HEADERSIZE, F_iTagSize);
    try
      if (li_Size = F_iTagSize) then begin
        repeat
          F_ReadFrame(@lp_Buff[li_Offset]);

li_Offset はフレームの頭だしをするためのオフセット値です。
フレームの頭がデータの頭からどれだけ離れているかを示しているとも言えます。
読み込んだフレームヘッダー中のフレームIDを判定して必要なタグかどうかを見てそれぞれのタグに合わせた取り出し方をします。
下の例ではコメントと歌詞とユーザー定義のテキストの三つは複数行のテキスト用の取り出し方を、それ以外は1行のテキスト用の取り出し方をしています。

          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で始まるフレームと違い複数行のテキストなので判定に加える
              //F_sUserText := F_GetFullText(@lp_Buff[li_Offset], F_iFrameSize);
            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;

F_ReadFrameでフレームを解析してみてそれがフレームではないと判断された(F_iFrameHeaderSize = 0)らデータの途中であったとしてもそこでタグは終了しているものとします。
というのも最後のフレームの後に余分な空白データ(パディングと言います)があることもあり、タグのサイズにはその分も含まれるからです。

タグの取り出し

'TXXX'をのぞく 'T' で始まるフレームIDのタグは改行を含まない1行テキストのみのタグです。
フレームデータは頭に文字エンコードの方法を識別する1バイトのデータがあるテキストになります。
つまり1バイトをスキップして(フレームサイズ -1)バイトを読み込まなければなりません。
文字エンコードは2.3までがShift-JISとBOMありのユニコード、2.4からはそれにBOMなしのUTF-16のビッグエンディアンとUTF-8が追加されました。

function TMyTagID3.F_GetText(pData: PAnsiChar; iCount: DWORD): WideString;
//改行を含まないテキストを返す
var
  li_Index, li_Enc: DWORD;
begin
  li_Enc := Ord(pData[0]);
  li_Index := 1; //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 else begin
    Result := '';
  end;
end;

改行を含む歌詞やコメントの取り出し方は1行テキストの場合と少し違います。
頭に文字エンコードの種類がつくのは同じですが、その後に3バイトの言語コードがあり、その次に「簡単な説明文」があり、その後からようやく「本文」の歌詞やコメントが始まります。
つまり「本文」を取り出すには文字エンコードの1バイトと言語コードの3バイトを読み飛ばすだけでなく「簡単な説明文」も読み飛ばさなければなりません。
「簡単な説明文」と「本文」の間は#0で区切られます。(Unicodeの場合は#0#0。UTF-8は#0)
「簡単な説明文」の扱いをどうするかの問題がありますが(一緒に取り出すのか別々に取り出すのか無視するのか)ここでは無視する方向で。

function TMyTagID3.F_GetFullText(pData: PAnsiChar; iCount: DWORD): WideString;
//USLT,COMMなどの改行を含むテキストを返す
var
  i, li_Enc, li_Index: DWORD;
begin
  li_Enc := Ord(pData[0]);
  li_Index := (1 + 3);  //文字エンコード(1Byte)と言語コード(3Byte)を読み飛ばす
  Dec(iCount, (1 + 3)); //文字エンコード(1Byte)と言語コード(3Byte)を減らす
  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);
      if (li_Enc = $3) then begin
        //UTF-8
        Result := UTF8Decode(Result);
      end;
    end else begin
      Result := '';
    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 else begin
      Result := '';
    end;
  end;
end;

その他にも各タグごと色々な取り出し方があります。

ソースコード

Ver 1、1.1、2.2、2.3、2.4 の読み込みに対応。
ただしヘッダーフラグやフレームフラグなどの細かい仕様(Unsyncや圧縮など)には非対応。
分割されたタグにも非対応。
Unicodeには対応。

interface
uses
  Windows;


type
  TMyTagID3 = class(TObject)
  private
    F_sFileName: WideString;

    F_iTagSize:         DWORD;        //全部のタグのサイズ
    F_sFrameID:         WideString; //フレームID
    F_iFrameHeaderSize: DWORD;        //フレームヘッダーのサイズ
    F_iFrameSize:       DWORD;        //フレームのサイズ

    //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 コメント/説明

    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;
    procedure F_ReadFrame(pData: PAnsiChar);
    procedure F_SetFileName(sFileName: WideString);
    procedure F_Clear;
    procedure F_ReadV1;
    procedure F_ReadV2;
  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;
    property Conductor:       WideString read F_sConductor;
    property Composer:        WideString read F_sComposer;
    property Writer:          WideString read F_sWriter;
    property Translator:      WideString read F_sTranslator;
    property Publisher:       WideString read F_sPublisher;
    property Genre:           WideString read F_sGenre;
    property Length:          WideString read F_sLength;
    property Year:            WideString read F_sYear;
    property Date:            WideString read F_sDate;
    property Lylic:           WideString read F_sLylic;
    property OriginalAlbum:   WideString read F_sOriginalAlbum;
    property OriginalWriter:  WideString read F_sOriginalWriter;
    property OriginalAuthor:  WideString read F_sOriginalAuthor;
    property OriginalRelease: WideString read F_sOriginalRelease;
    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_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_Clear;
//変数のクリア
begin
  F_iTagSize         := 0;  //タグ全体のサイズ
  F_sFrameID         := ''; //フレームID
  F_iFrameHeaderSize := 0;  //フレームヘッダーのサイズ
  F_iFrameSize       := 0;  //フレームのサイズ

  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 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 -1 - i]) * 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 -1 - i]) * lfniPower($100, i));
  end;
end;

procedure TMyTagID3.F_ReadHeader;
var
  lp_Buff: PAnsiChar;
  li_Size: DWORD;
begin
  li_Size := gfniFileRead(lp_Buff, F_sFileName, 0, 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]);
        F_iTagSize := F_GetSyncsafeSize(@lp_Buff[6], 4); //サイズは4バイト
        //このサイズにはヘッダー自身の10バイトは含まない
      end;
    end;
  finally
    FreeMem(lp_Buff);
  end;
end;

procedure TMyTagID3.F_ReadFrame(pData: PAnsiChar);
begin
  F_iFrameHeaderSize := 0;
  //フレームIDが正しくない場合はフレームヘッダーのサイズは0のまま
  if (pData[0] in ['0'..'9', 'A'..'Z']) then begin
    if (F_iMajor = 2) then begin
      //Ver2.2
      F_sFrameID := gfnsByteCopy(pData, 0, 3);
      F_iFrameHeaderSize := 3 + 3;
      //ID、サイズとも3バイトでフレームのヘッダーサイズは6バイト
      F_iFrameSize := F_GetSize(@pData[3], 3);  //サイズは3バイトのフレームIDの次の3バイトでSyncsafeではない
    end else if (F_iMajor >= 3) then begin
      //Ver2.3以上
      //ID、サイズとも4バイト、2バイトのフラグでフレームのヘッダーサイズは10バイト
      F_iFrameHeaderSize := 4 + 4 + 2;
      F_sFrameID := gfnsByteCopy(pData, 0, 4);
      if (F_iMajor = 3) then begin
        //Ver2.3
        F_iFrameSize := F_GetSize(@pData[4], 4);  //サイズは4バイトのフレームIDの次の4バイトでSyncsafeではない
      end else begin
        //Ver2.4以上
        F_iFrameSize := F_GetSyncsafeSize(@pData[4], 4);  //サイズは4バイトのフレームIDの次の4バイトでSyncsafe
      end;
    end;
  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; //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 else begin
    Result := '';
  end;
end;

function TMyTagID3.F_GetFullText(pData: PAnsiChar; iCount: DWORD): WideString;
//USLT,COMMなどの改行を含むテキストを返す
var
  i, li_Enc, li_Index: DWORD;
begin
  li_Enc := Ord(pData[0]);
  li_Index := (1 + 3);  //文字エンコード(1Byte)と言語コード(3Byte)を読み飛ばす
  Dec(iCount, (1 + 3)); //文字エンコード(1Byte)と言語コード(3Byte)を減らす
  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);
      if (li_Enc = $3) then begin
        //UTF-8
        Result := UTF8Decode(Result);
      end;
    end else begin
      Result := '';
    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 else begin
      Result := '';
    end;
  end;
end;


procedure TMyTagID3.F_ReadV2;
var
  lp_Buff: PAnsiChar;
  li_Size, li_Offset: DWORD;
  ls_Text: WideString;
begin
  if (F_iTagSize > 0) then begin
    li_Offset := 0;
    li_Size := gfniFileRead(lp_Buff, F_sFileName, lciTAG_HEADERSIZE, F_iTag); //10バイト目から読み込む
    try
      if (li_Size = F_iTagSize) then begin
        repeat
          F_ReadFrame(@lp_Buff[li_Offset]);
          Inc(li_Offset, F_iFrameHeaderSize);
          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[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 = 'TIT1') or (F_sFrameID = 'TT1') then begin
                //内容の属するグループの説明
                F_sSeries := ls_Text;
              end else if (F_sFrameID = 'TIT2') or (F_sFrameID = 'TT2') then begin
                //タイトル/曲名/内容の説明
                F_sTitle := ls_Text;
              end else if (F_sFrameID = 'TIT3') or (F_sFrameID = 'TT3') then begin
                //サブタイトル/説明の追加情報
                F_sSubTitle := ls_Text;
              end else if (F_sFrameID = 'TALB') or (F_sFrameID = 'TAL') then begin
                //アルバム/映画/ショーのタイトル
                F_sAlbum := ls_Text;
              end else if (F_sFrameID = 'TPE1') or (F_sFrameID = 'TP1') then begin
                //主な演奏者/ソリスト
                F_sAuthor := ls_Text;
              end else if (F_sFrameID = 'TPE2') or (F_sFrameID = 'TP2') then begin
                //バンド/オーケストラ/伴奏
                F_sBand := ls_Text;
              end else if (F_sFrameID = 'TPE3') or (F_sFrameID = 'TP3') then begin
                //指揮者/演奏者詳細情報
                F_sConductor := ls_Text;
              end else if (F_sFrameID = 'TCOM') or (F_sFrameID = 'TCM') then begin
                //作曲者
                F_sComposer := ls_Text;
              end else if (F_sFrameID = 'TEXT') or (F_sFrameID = 'TXT') then begin
                //作詞家/文書作成者
                F_sWriter := ls_Text;
              end else if (F_sFrameID = 'TPE4') or (F_sFrameID = 'TP4') then begin
                //翻訳者, リミックス, その他の修正
                F_sTranslator := ls_Text;
              end else if (F_sFrameID = 'TPUB') or (F_sFrameID = 'TPB') then begin
                //出版社, レーベル
                F_sPublisher := ls_Text;
              end else if (F_sFrameID = 'TCON') or (F_sFrameID = 'TCN') then begin
                //ジャンル
                F_sGenre := ls_Text;
              end else if (F_sFrameID = 'TLEN') or (F_sFrameID = 'TLE') then begin
                //長さ
                F_sLength := ls_Text;
              end else if (F_sFrameID = 'TYER') or (F_sFrameID = 'TYE') then begin
                //年
                F_sYear := ls_Text;
              end else if (F_sFrameID = 'TDAT') or (F_sFrameID = 'TDA') then begin
                //日付
                F_sDate := ls_Text;
              end else if (F_sFrameID = 'TOAL') or (F_sFrameID = 'TOT') then begin
                //オリジナルのアルバム/映画/ショーのタイトル
                F_sOriginalAlbum := ls_Text;
              end else if (F_sFrameID = 'TOLY') or (F_sFrameID = 'TOL') then begin
                //オリジナルの作詞家/文書作成者
                F_sOriginalWriter := ls_Text;
              end else if (F_sFrameID = 'TOPE') or (F_sFrameID = 'TOA') then begin
                //オリジナルアーティスト/演奏者
                F_sOriginalAuthor := 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;
          Application.ProcessMessages;
        until (li_Offset >= F_iTagSize) or (F_iFrameHeaderSize = 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