アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

Metadata Editor をつくる 6 - MP3 編 2

September 05, 2010開発.NET, ID3, Metadata Editor, MP3

Metadata Editor を自作するシリーズその 6。今回は MP3 の ID3v2 タグをより詳細に解析してゆく。ID3 Tag ページも随時更新しているけれど、あちらは参考資料として書いているので、具体的な実装などはこちらに書こうと思う。

前回からの変更点

  • ライブラリ名を Parade.Metadata から Owl に変更 →短くドットの入らない名前にすることにした →池袋駅でいけふくろうをぼんやり眺めて決めた
  • ライセンスを LGPL から修正 BSD ライセンスに変更
  • ユーザー定義テキスト、言語・説明つきテキストを解析
  • 非同期化されたデータの解除処理を修正
  • 圧縮フレームに対応するため、zlib.NET のソースを取り込む

ID3v2 における文字コード

ID3v2 では文字コード設定をもたない文字列は ISO-8859-1 となる。例えば W で開始される ID をもつ URL 系のフレームがこれに該当する。テキスト情報フレームなどは 1 バイトの文字コード設定が定義されており以下のように指定される。

対応バージョン 文字コード
v2.2 ~ 2.4 0 ISO-8859-1
v2.2 ~ 2.4 1 UTF-16 ( BOM 付き )
v2.4 2 UTF-16 ビッグ エンディアン
v2.4 3 UTF-8

値が 0 の場合でも ISO-8859-1 にしないソフトウェアがある。それらは処理系に依存した文字エンコーディングを使用しているようだ。日本語の Windows ならば Windows-31J が設定される。そのため厳密に ISO-8859-1 として扱うと文字化けすることになる。

この問題への対処としては

  • 文字コードを自力で判定する
  • 文字列を表示してユーザーに化けない文字コードを選ばせる

などの方法が考えられるが、どちらの方法もイマイチである。前者はタグ程度の長さでは精度に期待できず、後者は開発が大変なうえユーザビリティを著しく損ねる。きっと ISO-8859-1 にしていないソフトウェアもこのような葛藤があったのだろう。現実的な解決策はやはり処理系に依存したエンコーディングとして扱うこと。これは対象データを扱う環境間の処理系は統一されているであろう、という前提に基づく。

文字列の読み込みは基本的に System.Text.Encoding クラスの static プロパティで対応するエンコード名を持つものを使用すればよい。システム標準ならば Encoding.Default である。UTF-16 (BOM 付き) は BOM 判定が必要なので以下のような処理となる。

/// <summary>
/// BOM 付きの UTF-16 文字列を表すバイト配列を、文字列に変換します。
/// </summary>
/// <param name="value">バイト配列。</param>
/// <param name="index">バイト配列の変換を開始する位置。</param>
/// <param name="count">変換をおこなうバイト数。</param>
/// <returns>文字列。</returns>
string GetStringUtf16WithBom( byte[] bytes, int index, int count )
{
    // BOM 分 + 1 文字に満たないなら、空文字として扱う
    if( bytes == null || bytes.Length < 3 ) { return String.Empty; }

    // 適切な BOM が設定されていたら、後続バイトを文字列化する。
    if( bytes[ index ] == 0xFE && bytes[ index + 1 ] == 0xFF )
    {
        return Encoding.BigEndianUnicode.GetString( bytes, index + 2, count );
    }
    else if( bytes[ index ] == 0xFF && bytes[ index + 1 ] == 0xFE )
    {
        return Encoding.Unicode.GetString( bytes, index + 2, count );
    }
    else
    {
        return String.Empty;
    }
}

文字列の終端

ID3v2 は文字列が $00 または $00 00 で終端されることになっている。ただし id3v2.3.0 の仕様の表現は以下のようになっている。

If the textstring is followed by a termination ($00 (00)) all the following information should be ignored and not be displayed.

終端が含まれていた場合は以降の情報を無視して表示しない、ということなので、終端がタグの終わりと等しいことは保証されないようだ。部分更新のために、余白を入れるソフトウェアがあるかもしれない。そこで、読み取ったデータをすべて文字列化するのではなく、仕様に従って終端の直前までをデータとして扱うことにする。

以下のように終端を探して Encoding.GetString() はその直前までを読み取り範囲として指定。

/// <summary>
/// 指定されたバイト配列から文字列終端 ( ワイド文字なら $00 00、それ以外は $00 ) を探します。
/// </summary>
/// <param name="value">バイト配列。</param>
/// <param name="index">検索を開始する位置。</param>
/// <param name="count">検索の終了位置。</param>
/// <param name="isWideChar">バイト配列がワイド文字なら true。それ以外は false。</param>
/// <returns>成功時は終端位置。それ以外は -1。</returns>
int FindStringTerminate( byte[] value, int index, int count, bool isWideChar )
{
    var max = ( index + count < value.Length ? count : value.Length );
    for( var i = index; i < max; ++i )
    {
        if( value[ i ] == 0x00 && ( !isWideChar || ( i + 1 < value.Length && value[ i + 1 ] == 0x00 ) ) )
        {
            return i;
        }
    }

    return -1;
}

あまりにもベタな検索だけどタグ情報は最大でも数十文字ぐらいと考えられるので十分だろう。ちなみに ID3v2 でワイド文字になるエンコードは UTF-16 系である。

圧縮フレーム

フレーム ヘッダに圧縮フラグが立っているとそのフレームは zlib による圧縮がおこなわれている。これはいわゆる ZIP 圧縮のことなのだが zlib を名指ししている点に注意が必要である。

C# でフレームの圧縮・展開を考えた場合、.NET Framework 2.0 から追加された System.IO.Compression.GZipStream を利用できそうだが、このクラスは RFC-1952 に基づいて実装されている。一方、zlib は RFC-1951 である。1951 は Deflate、1952 は gzip であり、両者のバイナリ互換は保証されていない。

そこで今回は zlib の .NET 移植となる zlib.NET を利用することにした。

このライブラリは BSD スタイルのライセンスで提供されており言語は C#、モジュールは zlib.net.dll となる。はじめはモジュールを参照するか依存を最小にするために動的ロード (モジュールを読めたときだけ圧縮フレームをサポート) するつもりだったのだが、ソースそのものを流用することに決めた。

現在は zlib のソースに手を加えずそのまま Owl へ取り込んでいる。そのため zlib.NET の public クラスが Owl クライアントに見えてしまっている。これはいずれ internal にするかもしれないので Owl を修正 BSD ライセンスに変更した。LGPL だと copyleft の関係で BSD ライセンスと混在できない。

圧縮されたフレームの展開は以下のような処理となる。

/// <summary>
/// 圧縮されたデータを復元します。
/// </summary>
/// <param name="src">圧縮されたデータへのストリーム。</param>
/// <param name="dest">復元されたデータを書き込むストリーム</param>
void UnZip( Stream src, Stream dest )
{
    var reader = new ZInputStream( src );
    var buffer = new byte[ 1024 ];

    while( true )
    {
        var length = reader.read( buffer, 0, buffer.Length );
        if( length <= 0 ) { break; }

        dest.Write( buffer, 0, length );
    }
}

src にあたるストリームは圧縮されたデータの始点を指した状態の Stream、またあデータがバイト配列として取得されているならそれで初期化した MemoryStream を指定。dest が展開後のストリームとなるので、これは空の MemoryStream を指定することになるだろう。

今回の成果物

今回の成果物を以下に公開。ライセンスは 修正 BSDライセンスとする。

プロジェクトは Visual C# 2010 Express で作成。ビルドをおこなうと Owl.dll と TestApp.exe というコンソールアプリケーションが生成される。現時点の機能は以下のようになる。

  • ASF ( WMA や WMV ) のタグ編集、メタデータをテキストにダンプ
  • ID3v1 ~ 2.4 のメタデータをテキストにダンプ

今後の予定

ID3v1 ~ v2 のダンプを強化しつつエディタを実装してゆく。NUnit によるユニットテスト、サンプルを GUI つきにすることを検討中。

Comments from WordPress

  • gageas 2010-09-27T22:56:32Z

    もう既に修正されているかもしれませんが、ソースを見ていて3点気になったことがありました。
    TagDumper.csについてですが、249行目DumpFrameV23内の

    var info = Id3DefinedTags.V22.GetInfo( id );

    var info = Id3DefinedTags.V23.GetInfo( id );

    の誤りではないでしょうか?

    同様に295行目も

    var info = Id3DefinedTags.V24.GetInfo( id );

    ではないでしょうか。

    2点目ですが、非同期化の扱いにも誤りがあるようです。

    ID3V2.3ではタグ全体を非同期化しますが、ID3V2.4ではフレームごとに非同期化を行います。詳細は
    http://www.id3.org/id3v2.4.0-changes?action=raw の 3. Tag structure changes
    および
    http://www.id3.org/id3v2.4.0-structure?action=raw の 6. Unsynchronisation
    を見ていただければよいかと思います。

    3点目は、これも2.4で非同期化がフレームごとになったことに関連するのですが、V2.4ではフレームヘッダのsizeフィールドは下位7ビットのみを使用するように変更になっています。

    これについても上記id3.orgのドキュメントを見ていただければよいと思います。
    以上3点ほど気がつきましたのでご報告させていただきます。

  • akabeko akabeko 2010-09-27T23:33:53Z

    ご指摘ありがとうございます。このような報告は非常に助かります。

    問題点 1 は修正しました。問題点 2 の非同期化は、設計も含めてどう修正しようか検討中です。問題点 3 については、フレームヘッダの仕様を再確認します。別ページに書いている ID3 タグのメモにも説明が必要ですね。

    次にこのシリーズで記事を書くときは、報告をいただいた 3 点の修正を反映する予定です。