Metadata Editor をつくる 6 – MP3 編 2

2010年9月5日 2 開発 , , , ,

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 の場合、ソース再配布時に LGPL が強制されるので 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

  • 2010年9月28日 7:56 AM

    もう既に修正されているかもしれませんが、ソースを見ていて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点ほど気がつきましたのでご報告させていただきます。

  • 2010年9月28日 8:33 AM

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

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

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