アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

Metadata Editor をつくる 4

August 01, 2010開発.NET, ASF, Metadata Editor, WMA

Metadata Editor を自作するシリーズその 4。前回から 2 週間ぐらい空いてしまったが、なんとか編集の基本部分が実装できたので公開。まだ書き込めるデータが少ないけれど定義さえ分かれば簡単な変更でサポートするデータを増やしてゆけるように設計している。そのため他のメタデータ形式 (ID3 など) の対応を検討しつつ ASF も併行して調査 & 対応強化してゆきたい。

前回からの変更点

  • タグ情報のキャッシュ方法を変更
  • タグの書き込みと保存をサポート
  • ダンプする情報をより詳細にした

サポートしているタグ情報については ASF File のタグ情報リストを参考のこと。このリストは随時更新する予定。書き込みを実装していて、厳密なサイズやストリーム上の位置を知る必要などがあったので、ダンプ情報はかなり詳細まで出力するようにしている。

タグ情報のキャッシュ

前回までは、すべてのタグ情報についてストリーム上の位置とサイズを記録して遅延読み込みをおこなうようにしていた。今回からは固定長のデータについては遅延せずにキャッシュすることにした。

遅延用のデータは位置が long で 8、サイズが int で 4 の計 12 バイト。しかし固定長のデータでこれを超えるのは Guid の 16 バイトのみだから、これらはキャッシュしてもよいと判断した。可変長のデータについては従来通り遅延しキャッシュもおこなわない。

この処理に対応するため、タグ情報クラスを以下のように定義する。

/// <summary>
/// タグ情報を表します。
/// </summary>
class TagData
{
    /// <summary>
    /// インスタンスを初期化します。
    /// </summary>
    /// <param name="value">タグの内容。</param>
    public TagData( object value )
    {
        this._value = value;
    }

    /// <summary>
    /// インスタンスを初期化します。
    /// </summary>
    /// <param name="position">ストーリム上の読み出し位置 ( バイト単位 )。</param>
    /// <param name="length">ストリームから読み取るサイズ ( バイト単位 )。</param>
    /// <param name="type">データ型。</param>
    /// <param name="innerType">ファイル内のデータ型。</param>
    /// <param name="type">タグ情報を読み込むためのストリーム。</param>
    public TagData( long position, int length, TagDataType type )
    {
        this._value = new ReadInfo( position, length, type );
    }

    /// <summary>
    /// タグ情報の編集がおこなわれたことを示す値を取得します。
    /// </summary>
    public bool IsEdited { get; private set; }

    /// <summary>
    /// タグの内容を取得または設定します。
    /// </summary>
    public object Value
    {
        get
        {
            return this._value;
        }
        set
        {
            this._value   = value;
            this.IsEdited = true;
        }
    }

    /// <summary>
    /// タグの内容。遅延読み込みをおこなう場合は ReadInfo を格納する。
    /// </summary>
    private object _value;

    /// <summary>
    /// 遅延読み込みをおこなうための情報を格納します。
    /// </summary>
    public class ReadInfo
    {
        /// <summary>
        /// インスタンスを初期化します。
        /// </summary>
        /// <param name="position">ストーリム上の読み出し位置 ( バイト単位 )。</param>
        /// <param name="length">ストリームから読み取るサイズ ( バイト単位 )。</param>
        /// <param name="type">データ型。</param>
        public ReadInfo( long position, int length, TagDataType type )
        {
            this.Length    = length;
            this.Position  = position;
            this.Type      = type;
        }

        /// <summary>
        /// ストリームから読み取るサイズ ( バイト単位 ) を取得します。
        /// </summary>
        public int Length { get; private set; }

        /// <summary>
        /// ストーリム上の読み出し位置 ( バイト単位 ) を取得します。
        /// </summary>
        public long Position { get; private set; }

        /// <summary>
        /// データ型を取得します。
        /// </summary>
        public TagDataType Type { get; private set; }
    }
}

これを考慮した例となる Extended Content Description の読み込み処理。

/// <summary>
/// Extended Content Description Object の内容を読み取ります。
/// </summary>
/// <param name="reader">ASF のタグ情報を読み取るためのストリーム。</param>
private void LoadExtendedContentDescription( Stream reader )
{
    ushort count = reader.ReadUInt16();
    for( ushort i = 0; i < count; ++i )
    {
        var name   = reader.ReadStringUtf16( reader.ReadUInt16() );
        var type   = ( TagDataType )reader.ReadUInt16();
        var length = reader.ReadUInt16();

        // 非公開のタグは読み飛ばす
        var info = AsfDefinedTags.GetInfo( name );
        if( info == null )
        {
            reader.Seek( length, SeekOrigin.Current );
            continue;
        }

        // 可変長のデータは遅延、それ以外はここで読み込む
        if( info.Type == AsfTagDataType.Binary || info.Type == AsfTagDataType.String )
        {
            this._tags.Add( name, new TagData( reader.Position, length, type ) );
            reader.Seek( length, SeekOrigin.Current );
        }
        else
        {
            this._tags.Add( name, new TagData( this.Read( reader.Position, length, info.Type, type, reader ) ) );
        }
    }
}

TagData.Value の内容がキャッシュと TagData.ReadInfo に分岐することとなる。TagData.Valueobject 型にしてあるので、これを as 演算子で TagData.ReadInfo へダイナミック キャスト。成功すれば遅延、null 参照ならキャッシュ済みということになる。よって値の読み込み処理は以下のようになる。

/// <summary>
/// タグ情報を読み取ります。
/// </summary>
/// <param name="name">タグ名。</param>
/// <returns>成功時はタグ情報。それ以外は null 参照。</returns>
public object Read( string name )
{
    // 未定義のタグは対象外
    var info = AsfDefinedTags.GetInfo( name );
    if( info == null ) { return null; }

    TagData data;
    if( !this._tags.TryGetValue( name, out data ) ) { return null; }

    var value = data.Value as TagData.ReadInfo;
    return ( value == null ? data.Value : this.Read( value.Position, value.Length, info.Type, value.Type, this._reader ) );
}

メソッド末尾がキャッシュと遅延の分岐。キャスト失敗なら TagData.Value をそのまま返し、成功時はオーバーロードされた this.Read(...) メソッドを介して遅延読み込みする。

ファイルへの書き込み処理

基本的なデータ管理は前回を参照のこと。AsfTagEditor クラスは編集対象となるファイルへの読み取りストリームを保持している。タグの編集内容を保存するときはここからデータを読み取りつつ、以下のように書き込む。

  1. 編集対象ファイルと同じフォルダに一時ファイルを生成

    • 名前の生成は Path.GetRandomFileName() でおこなう
    • このファイルに編集結果を書き込み、最後に元ファイルを置き換える
    • 異なるドライブだと置き換え時にコピーが発生するので同一フォルダにしておく
  2. 一時ファイルへの書き込みストリームを生成
  3. 読み取りストリームからデータを読み出す
  4. 書き込みストリームにデータを書き出す

    • 未編集のタグなら読み取ったデータを引き継ぐ
    • 編集済みならそれを書き込む
    • 読み取ったデータがタグとして存在しないなら書き込まない ( 削除となる )
  5. Object 単位で編集対象外、または Header Object 以降のものは読み取りストリームの内容をそのまま引き継ぐ
  6. すべてのデータ書き出し完了
  7. 書き込みストリームを閉じる

    • 一時ファイルに編集内容が保存される
  8. 読み取りストリームを閉じる
  9. 編集対象ファイルを一時ファイル名にリネーム
  10. 編集内容を書き込んだ一時ファイルを正式なファイル名へリネーム
  11. 一時ファイル名となっている編集対象ファイルを削除
  12. 新しいファイルに対して読み取りストリームを生成

    • 以降はこれが編集対象となる

サイズ更新

ASF タグを編集したら格納箇所のサイズ情報を更新しなければならない。対象は以下の 3 箇所。

  • 各 Object のサイズ
  • Header Object のサイズ
  • ファイルのサイズ

基本的な処理の流れとしては以下のようになる。

  1. サイズを格納している部分のストリーム上の位置を記録
  2. 各種データを書き込みつつ、そのサイズ累積を保持 & 更新
  3. 現在のストリーム位置を記録
  4. サイズ格納位置へ移動してサイズ累積を書き込む
  5. 元のストリーム位置へ戻る

各 Object のサイズを上記の処理で更新してゆき Object サイズの累積を Header Object のサイズへ反映する。ファイルすべての書き込みが完了したら、書き込みストリームのデータ長をファイルサイズへ反映する。これらの処理のひとつでも失敗すると ASF ファイルとして破損するので注意する。正しくタグ情報を書き込んでいてもサイズ設定が不適切な場合は、他のプログラムが正しくデータを読めない。

Header Object のサイズと後続の Object 群のサイズ合計がファイルサイズと一致していれば成功である。

今回の成果物

今回の成果物を以下に公開。

ライセンスは GNU Lesser General Public License とする。ただし .NET Framework の場合はコンパイル時にライブラリとプログラムを同一バイナリに結合できない (ILMerge) ので、Parade.Metadata.dll を改変なしでリンクする場合、利用するプログラム側には GPL/LGPL を適用せず、配布も制限しない。

プロジェクトは Visual C# 2010 Express で作成。現時点の機能としては、ASF ファイルの編集とデータのダンプをサポート。ビルドをおこなうと Parade.Metadata.dll と TestApp.exe というコンソールアプリケーションが生成される。

今後の予定

ASF でサポートするデータを増やしつつ MP3 などで利用されている ID3 タグの編集機能を実装したい。

ASF としてジャケット画像 (WM/Picture) やタイムラインつきの歌詞情報 (WM/Lyrics_Synchronised) をサポートしたいところ。しかし ID3 とデータ形式をすりあわせられるかもしれないので、これらは先送りするかも。現時点の実装でもこれらはバイト配列として取れるのだが、どのような構造でマップすべきかは MP3 のデータ構造を見てから決めたい。

より大きな予定としては ASF → ID3 → その他 (AAC or Ogg Vorbis、FLAC、...etc) を検討している。