wavをバイナリから読み解く
wavとは
音声情報を記述するファイルフォーマットの一つ。mp3などと違い、(たいてい)圧縮されていない生の離散波形データが入っています。この記事では、このwavフォーマットにはどのようなデータがどのように格納されているのかを調べます。
対象読者
RIFF形式でフォーマットされたバイナリファイルなどを読んだことがない方、音声ファイルフォーマットに興味のある方などが想定読者です。初めてバイナリファイルを読む人でも大丈夫だと思います。
まず眺める
こういうのは実際に本物のデータをいじくるのが良いと思います。
ちょうど手元にwaveファイルがあったので、これを解剖することにします。
VScodeにバイナリ用の拡張を入れる
デフォルトのVScodeでwaveファイルを開こうとすると、次のように怒られます。
Hex Editor というVScode公式の拡張を入れることで、バイナリを快適に表示できます。
この時点で、「WAVE」などの文字列が確認できますね(画像右側、Decorded Text部を参照)。
RIFFフォーマットについて
waveのファイルフォーマットを説明するにあたって、まず「RIFF」フォーマットについて説明する必要があります。
RIFFとは、複数の「チャンク」の入れ子構造によってファイルを構成するメタファイル形式のことで、waveはこの構造を採用しています。実際のRIFF形式のファイルの基本構造は以下のようになります。(参考)
このように、RIFF形式のファイルでは「RIFFチャンク」の中にサブチャンクを連ねることでデータを記述しています。
それぞれのチャンクは自身のチャンク型を示すID、data領域の大きさを示すSize、そしてData領域からなります。IDは4byte文字列、Sizeは4Byte整数で表されます。data領域の中身はチャンク型によって異なります。
RIFFチャンク
ここでは、waveファイルの一番外側のチャンクであるRIFFチャンクをみながら、チャンクの構造について慣れます。
ID領域
前述のように、waveファイルの一番先頭にはRIFFチャンクのIDである"RIFF"があるはずです。
確認できますね。UTF8において"RIFF"は、52 49 46 46です。
このIDのおかげで、ファイルが不正なものでないことをある程度保証できます。
このような、ファイルフォーマット毎に固有の位置にある固有の値はマジックナンバーと呼ばれています。アプリケーションがファイルの種類を判断するとき、このマジックナンバーを参照しています。
Size領域
RIFFチャンクのIDが確認出来たら、次の4byteにあるSize領域を見ていきましょう。
RIFF形式において、複数バイトで表現される数値はリトルエンディアンです。
この場合は0x00574608を表しています。この値は10進数で5719560ですから、このファイルの残りの領域には5719560byteのデータがある、ということが分かります。
これに、IDとSize分の8byteを足した値がファイル全体のサイズです。
この値は、エクスプローラーから確認できるファイルサイズと一致します。(同じところを参照しているので当然)。
Data領域
RIFFチャンクのData領域にはまず、4Byteのファイルタイプ識別子が記述されます。
今回は"WAVE"です。
RIFFチャンクには、この後に複数のサブチャンク(fmtチャンク、dataチャンクなど)を記述することができます。
基本的にサブチャンクの順は決まりがありません。しかし、fmtチャンクだけはDataチャンクの直前に置く必要があります。
fmtチャンク
次に注目すべきはfmtチャンクです。waveファイルのfmtチャンクは、それ自身の大きさに応じて複数のフォーマットがあります。
size | format |
---|---|
14byte | WAVEFORMAT |
16byte | PCMWAVEFORMAT |
18byte | WAVEFORMATEX |
40byte | WAVEFORMATEXTENSIBLE |
先ほどのファイルの該当部を見ると、sizeは16byteだったので、これはPCMWAVEFORMATで書かれているとわかります。
以下に、WAVEFORMATとPCMWAVEFORMATの定義を記載します。
typedef struct waveformat_tag {
WORD wFormatTag;
WORD nChannels;
DWORD nSamplesPerSec;
DWORD nAvgBytesPerSec;
WORD nBlockAlign;
} WAVEFORMAT;
typedef struct pcmwaveformat_tag {
WAVEFORMAT wf;
WORD wBitsPerSample;
} PCMWAVEFORMAT;
fmtチャンクのデータ領域には、これらの音響信号を解釈するにあたって必要な様々な情報が入っています。WORD型は2byte,DWORD型は4byteの符号なし整数を持ちます。
この定義を見ると、fmtチャンク先頭の2byteにはwFormatTagが記載されていると分かります。これは、波形データのフォーマット方法を示しています。
0x0001はPCMですね。他にも波形フォーマットは多数あるようですが殆どが廃止されており、基本的にはPCMが採用されています。
次の2byteにはnChannelsの値が格納されています。今回は0x0002です。
このwavファイルはステレオ音源であることがわかりました。
他にも、サンプリングレートや量子化ビット数などの情報はこのfmtチャンクに入っています。
Dataチャンク
IDとSizeが記載されたのち、波形データが続きます。データ形式はfmtチャンクで述べられたものに従います。
Listチャンク
上記で述べた以外の情報をファイルに含めるときは、このLISTチャンク内に書かれます。LISTチャンクはRIFFチャンク以外で唯一、内部にサブチャンクを持つことができるチャンクなので、optionalなメタ情報の記述に便利です。
このLISTチャンクには様々な形が存在しています。"INFO"型が有名なようですが、今回は"adtl"型でした(asociated data list)。
見ると、data領域にTEMPO: 120.0とあります。このファイルをDAWソフトでファイル書き出しを行った際に書き込まれた情報でしょう。
adtl型は紐づくデータをCUEチャンクに格納できるのですが、今回は空データしか格納されていませんでした。
一方で、"INFO"型のLISTチャンクでは、データラベルとテキストが交互に格納される直感的な形式です。作曲者などの情報が入ることが多いです。
おまけ JUNKチャンク
前項までで、主要なチャンクの解説はすべて行いました。
しかし今回見ていたファイルには、"JUNK"というチャンクがありました。
これは、waveファイル運用の歴史が絡んでいます。
先ほど説明した、曲のメタ情報などが格納されているListチャンクの位置がカギになります。
WAVの企画を定めているマイクロソフトはLISTチャンクをファイルの先頭付近におくことを主流としていますが、多くのソフトは末尾に記述します。そのほうが、書き換えが早いからです。
そして、本来冒頭にLISTチャンクがあったはずの領域にJUNKチャンクを配置することで、他のチャンクの位置がズレないようにしているのです。
おわりに
wavファイルを題材に、バイナリファイルを探索しました。MP3などに比べればそこまで複雑な構成ではないので、バイナリデータを読み解く最初の経験としてはとても良いと思います。あなたもぜひ、お手元のwavファイルをバイナリエディタに突っ込んでみてください。
Discussion