title.png


WAV、JPEGファイルを解析する

 Olive+ ver.4.16(R11)で追加した機能を使って、WAVファイルとJPEGファイルの解析をするスクリプトを組んでみたいと思います。

※Olive+ R11より古いリリースだと動作しませんので、ダウンロードコーナから最新版を入手してください。


WAVファイルの解析

RIFFファイル形式

 まずはWAVファイルから。WAVファイルは、RIFFと言うファイル形式の1つで、データがチャンクと呼ばれる塊になって記録されています。ざっと、RIFFファイルの形式について説明します。
 チャンクは、4文字のチャンクIDに続き、4バイトのチャンクサイズと、その後ろに<チャンクサイズ>バイトのデータが続きます。

chunk.png

 チャンクIDが"LIST"であるチャンクの場合は、チャンクが階層化(グループ化)されて、内部にサブチャンクを持ちます。

list.png

 RIFFファイル全体はどうなっているかと言うと、ID"RIFF"で始まる、大きなチャンクになっています。

riff.png

 見た目としては、最上位にRIFFチャンクがあって、その下にサブチャンクが階層化されている。サブチャンクは、LISTチャンクによってさらに階層化される。

WAVファイル

 WAVファイルはどうなっているかと言うと、RIFFチャンクの下に、"fmt "(最後にスペース0x20)チャンクと、"data"チャンクがあって、"fmt "チャンクにデータの構造が定義され、実際のデータが"data"チャンクに格納されています。WAVファイルを生成したツールによっては、他にもチャンクが生成され、色々な付随情報を持ったものがあるようですが、オプションなので、"fmt "と"data"チャンクがあれば、WAVファイルとしての体裁は整うようです。
 "fmt "チャンクの構造ですが、以下の様になっています。

識別子 サイズ 意味
wFormatTag 2 WAVEデータの種類(形式)
nChannels 2 チャンネル数:モノラル=1、ステレオ=2
nSamplesPerSec 4 サンプリングレート(周波数)[Hz]
nAvgBytesPerSec 4 データレート[バイト/s]
nBlockAlign 2 ブロックサイズ
wBitsPerSample 2 1サンプル当たりのビット数
cbSize 2 拡張データサイズ

 "data"チャンクに格納されたデータがどういう形式なのかは、wFormatTagやwBitsPerSample等によって異なります。


解析スクリプト

 WAV(RIFF)ファイルの形式が(なんとなく)わかったところで、ファイル内のチャンク構造と、"fmt "チャンクの設定値を表示するスクリプトを作ってみます。

ファイルを開く

 fopen命令を使って、ファイルを開きます。

  // ファイルを開く
  var fin;
  fopen fin,$(fpath),"r";
  if (fin<0)
  {
    errmsg "WAV(RIFF)ファイルが開けなかった";
    leave;
  }
  var fout;
  fopen fout,$(fpath)".txt","w";
  if (fout<0)
  {
    errmsg "出力ファイルが開けなかった";
    leave;
  }


 対象のWAVファイルを読み込み("r")モードで、結果を書き出すファイルを書き込み("w")モードで開きます。開いたファイルは、ファイルディスクリプタを使ってアクセス(操作)しますので、fopen命令を実行する時にディスクリプタを受け取る変数名(ここではfinとfout)を渡しています。何らかのエラーでファイルが開けなかった場合、ディスクリプタに-1がセットされるため、エラーチェックしています。
 ちなみに、ファイルパスを問いあわせる記述は以下のような感じです。

  // WAV(RIFF)ファイルを問いあわせる
  var rtn;
  macro fpath;
  gui openfile,rtn, "WAV(RIFF)ファイルを指定してください","",fpath;
  if (rtn=-1)
  {
    // Cancel
    leave;
  }

RIFFファイルを読み込む

 まずは、ファイルの先頭から4バイト読み込んで、"RIFF"かどうかをチェックします。

  procedure fgetid fin,$$id_buf
  {
    // ファイルfinから、文字コード4バイト(チャンクID等)を
    // 読み込んで、マクロ$(id_buf)に格納する。
    // EOFに到達すると、空文字""をセットする。
    // 文字ではないところを読んで、4バイト全てが非印刷可能文字だった
    // 場合も、空文字が返ることに注意。
    mlet $(id_buf)="";
    var dat;
    fgetb fin,dat;
    chrcat $(id_buf),dat;
    fgetb fin,dat;
    chrcat $(id_buf),dat;
    fgetb fin,dat;
    chrcat $(id_buf),dat;
    fgetb fin,dat;
    chrcat $(id_buf),dat;
    if (dat=EOF)
    {
      mlet $(id_buf)="";
    }
  }


 この手続きは、

  fgetid <Fdesc>, <macro> ;

と、言う書式で、ファイル<Fdesc>から4バイト読み込んで、マクロ<macro>にデータをセットします。fgetidの第二引数はマクロ名なので、それを文字列として受け取って($$id_buf)います。procedure定義の引数には、通常は引数(数値)を受け取るための変数名を記述します。文字列引数を受け取る場合には、受け取るためのマクロ名(id_buf)と、それが文字列引数だと知らせるための記号($$)を記述するようになっています。例えば、

  macro dat;
  fgetid file_in, dat;


と、いう呼び出しに対して、上で定義した手続きfgetid内では、変数finには呼び出し元の変数file_inの値、マクロid_bufには、文字列"dat"がセットされています。手続き内で、

  mlet $(id_buf)="";

  ↓は、マクロ$(id_buf)が展開されて、

  mlet dat="";

と、解釈されて動作します。つまり、手続きの内部で、手続きの外で定義されたマクロdatを操作しているのです。結果として、引数として渡されたマクロdatに4バイトのIDがセットされることになります。
 同じテクニックが、16、32ビットの整数値データを読み込む手続きfget16、fget32にも使っていますので、参照してください。「引数のポインタ渡し」みたいな感じだと思ってください。
 さて、こうして読み込んだIDが"RIFF"以外だった場合は、エラーを表示して終わります。

  procedure analyze_riff
  {
    var cmp;
    var siz;
    var n_ind;  // インデントの深さ
    macro buf;

    // RIFF IDを読み込む
    fgetid fin,buf;
    strcmp cmp=$(buf),"RIFF";
    if (cmp=0)
    {
      // データサイズ
      fget32 fin,siz;
      // データタイプ
      fgetid fin,buf;

      mlet NUM_FORM="%08x";
      fprint fout,"(#) ",0;
      indent;
      fprint fout,"RIFF # "$(buf)$,,siz;

      analyze_chunk fin,siz - 4;
    }
    else
    {
      // ERROR: RIFFファイルではない
      fprint fout,"Not a RIFF file."$,;
      return;
    }
  }


 RIFFファイルだった場合には、まず、データサイズ(32ビット整数値)とデータタイプ(4バイト)を読み込んで、サブチャンクの解析に移ります。

  procedure fget32 fin,$$dat
  {
    // ファイルfinから32ビットの数値を読み込んで、
    // 変数$(dat)にセットする。数値はリトルエンディアン。
    // EOFに到達すると、-1をセットする。
    var dat2;
    fgetb fin,$(dat);
    fgetb fin,dat2;
    $(dat)=@(dat2<<8 + $(dat));
    fgetb fin,dat2;
    $(dat)=@(dat2<<16 + $(dat));
    fgetb fin,dat2;
    $(dat)=@(dat2<<24 + $(dat));
    if (dat2=EOF)
    {
      $(dat)=EOF;
    }
  }


 ファイルを操作する命令として、fgetb命令が追加となっています。今まで存在した、ファイルからの読み込み命令(fgets, fgetl等)は、ファイルをテキストファイルと見なして読み込みを行っていました。fgetb命令だけは、ファイルをバイナリファイルと見て、1バイトのデータを読み込み、データを返します。
 データサイズは32ビットのリトルエンディアンですので、fgetb命令で読み込んたデータを下位桁から順に積み上げていく形となります。「$(dat)=@(dat2<<8 + $(dat));」辺りで、シフト量を0→8→16→24と変化させながら、データを取り込んでいきます。
 この数式に現れた「@( )」と言う書式は、整数値に対してビット毎の論理演算やビットシフト演算などを計算することができる、バイナリ演算部です。通常の数式は全て、実数(浮動小数点数)値として演算していますが、「@( )」の内側では、整数演算を行います。「@<数値>( )」の書式で、指定したビット幅<数値>での符号付整数演算、「@@<数値>( )」の書式で、指定したビット幅<数値>での符号無整数演算を実行します。

サブチャンクの解析

 サブチャンクの解析手続きには、上位から、データのバイト数を引数として受け取ります。階層化されているので、現在のファイルポインタから解析を進めて、渡されたデータサイズをオーバしたら、その階層の解析は終了して戻るようにします。
 手順としては、チャンクIDを読み込む、チャンクサイズを読み込む、この時点のファイルポインタを取得しておく。

  // チャンクIDを読み込む
  fgetid fin,buf;
  // EOF?
  strcmp cmp=$(buf),"";
  if (cmp=0)
  {
    break;
  }
  // チャンクサイズ
  fget32 fin,siz;
  // ファイルポインタを取得
  ftell fin,fpos;


 次に、読み込んだIDが"LIST"だった場合には、更に階層化されているので、フォームタイプを読み込んで、表示した後で、再帰的にチャンクを解析する手続きを呼び出します。

  // サブチャンクの解析
  strcmp cmp=$(buf),"LIST";
  if (cmp=0)
  {
    // LISTチャンク
    macro form_typ;
    fgetid fin,form_typ;

    mlet NUM_FORM="%08x";
    fprint fout,"(#) ",fpos - 8;
    indent;
    fprint fout,$(buf)" # "$(form_typ)$,,siz;

    analyze_chunk fin,siz - 4;
  }


 "fmt "チャンクだった場合は、定義内容を読み込んで、表示します。

  strcmp cmp=$(buf),"fmt ";
  if (cmp=0)
  {
    // WAVファイルのfmtチャンク
    var fmt;
    var chn;
    var spl;
    var bps;
    var alg;
    var wid;
    fget16 fin,fmt;
    fget16 fin,chn;
    fget32 fin,spl;
    fget32 fin,bps;
    fget16 fin,alg;
    fget16 fin,wid;

    mlet NUM_FORM="%g";
    fprint fout,"  fmt=# chn=# spl=# Bps=# alg=# wid=#"$,,fmt,chn,spl,bps,alg,wid;
  }


 もう1つ注意することは、チャンクのスタートする位置です。チャンクIDは、常に偶数バイト境界から始まらなくてはならない決まりがあります。一方、チャンクサイズは、チャンク内の有効データ数を示しています。つまり、何を言いたいか?と言うと、もし、チャンクサイズが奇数バイトだった場合、そのまま次のチャンクを記録してしまうと、チャンクIDが奇数バイトから始まってしまうので、1バイトのダミーデータが追加されるのです。
 と、いうことで、チャンクサイズを取り込んだ直後に取得したファイルポジションfposと、取り込んだチャンクサイズsizを使って、次のチャンクの先頭アドレスまで移動する次の記述で、偶数境界への切り上げを実行しています。

  // ファイルポインタをsiz(偶数境界に切り上げ)だけ進める。
  fseek fin,fpos + @((siz + 1)/2)*2;

解析させてみる

 WAVファイル名(拡張子含む)に".txt"を追加したファイルが出力結果です。WAVファイルは単純なので、解析結果は以下のような感じです。

  (00000000) RIFF 000216d8 WAVE
  (0000000c)   fmt 00000012
    fmt=1 chn=1 spl=22050 Bps=44100 alg=2 wid=16
  (00000026)   data 000216b2

 最初の( )内は、ファイルポインタの値が16進数表示されています。バイナリファイルエディタなどで実際のファイルを観察する時にこのアドレスにjumpすると、チャンクIDを見つけることができます。このWAVファイルは、Microsoft PCM形式(fmt=1)、モノラル(chn=1)、サンプリングレート22.05[kHz](spl=22050)、データの精度16ビット(wid=16)、でした。
 うん。簡単すぎて面白くないですね。と、いう事で、AVIファイルを解析してみましょう。

  (00000000) RIFF 04b00e00 AVI
  (0000000c)   LIST 000000de hdrl
  (00000018)      avih 00000038
  (00000058)      LIST 00000092 strl
  (00000064)         strh 00000038
  (000000a4)         strf 00000028
  (000000d4)         strn 00000016
  (000000f2)   JUNK 000006fa
  (000007f4)   LIST 04b00204 movi
  (00000800)      00db 0012c000
  (0012c808)      00db 0012c000
     : (中略)
  (048a89f0)      00db 0012c000
  (049d49f8)      00db 0012c000
  (04b00a00)   idx1 00000400

 AVIファイルもRIFF形式の一種で、WAVよりはだいぶ複雑です。WAVファイルは、RIFFの後のフォームタイプが"WAVE"でしたが、AVIファイルの場合は、"AVI "になっています。まあ、これだけだとチャンクの構造しかわからないので、解析結果の有用性については大いに疑問ですが、「Olive+のスクリプト言語でバイナリファイルの解析ができる」ことが分かった、という事ですね。
 元々、WAVファイルの解析が目的だったので、ここで止めておきますが、MSDNなどでAVIファイルフォーマットを検索すると、もう少し解析できると思います。興味があれば、拡張してみてください。


完成スクリプト

WAV(RIFF)ファイルの解析

 完成スクリプトを以下から参照できます。

  Olive+スクリプト:analyze_wav.olv

JPEGファイルの解析

 応用でJPEGファイルの解析スクリプトも作ってみました。

  Olive+スクリプト:analyze_jpeg.olv

 実行結果はこんな(↓)感じです。

  実行結果を見る

 APPnマーカなどで、データ内のASCII文字列を探して表示する手続きstringsがあるのですが、大した処理をしていないため、文字列ではないところも文字列と誤判定してしまい、意味不明なデータを垂れ流す場合があります。ご容赦ください。
 途中、SOSマーカを発見したところで、解析を終了しています。SOSマーカの後ろには、EOIとRSTnマーカくらいしか登場しないはずなのに、読み込んで処理する時間はいっぱしにかかるので……。if文のtrue→falseにすると、最後まで解析するようになります。


download

Olive+