m13o.net

2020-12-08 Tue 21:08
Rustでwavを解析する その1 wavそれは   AdventCalendar2020 Rust programming

というわけで, wavの解析をしていきたいと思います.

wavはオーディオデータを記述, 格納するためにMicrosoftとIBMによって作られたRIFFというフォーマットの一種です. wav以外にも, avi等にも利用されているこのRIFF, 中々厄介なフォーマットで, RIFFの仕様含め, 技術文献がMicrosoftのサイト内でも体系的なものをみつける事が困難ですが, 一定見付けられる範囲で参考文献を以下に上げておきます.

RIFF
Resource Interchange File Format Services
WAVEFORMAT
WAVEFORMAT structure (mmreg.h)
WAVEFORMATEX
WAVEFORMATEX structure
Windows Multimedia
Windows Multimedia
AVI
AVI RIFF File Reference

また, wav形式の拡張であるBWF形式のRIFFの資料も参考になります.

さて, RIFFはチャンクという構造を並べたフォーマットとなっており, FourCCと呼ばれるチャンクのマジックナンバー4バイトとチャンクサイズを示す4バイトから構成されるヘッダを必ず持っており, チャンクサイズの次のバイト列からRIFFのデータがサイズ分収まっています. 例外はRIFFチャンクとLISTチャンクで, この2つのチャンクはその中に1以上のチャンク(サブチャンク)を内包する事ができるチャンクとなっており そのサブチャンクの構成がどういった物であるかを示す識別子が, チャンクサイズを示すバイト列の次の4バイトのバイト列となっており, その次のバイト列からサブチャンクが並びます. なお, このチャンクサイズはFourCCとチャンクサイズ自身のサイズ(つまり合計8バイト)を含みません.

RIFFのデータを読み解く処理としては大枠以下のような流れになります.

  1. 先頭4バイトを取得し, RIFFのFourCCの有無を確認
  2. 次の4バイトを取得し, データサイズを確認
  3. 次の4バイトを取得し, RIFFのデータ形式が何であるかを確認
  4. 次の4バイトを取得し, サブチャンクが何であるかを確認
  5. 次の4バイトを取得し, データサイズを確認
  6. サブチャンクがLISTならば, 4に戻る
  7. 6. でないならば, 5のデータサイズまでバイト列をそのサブチャンクの種別毎に解析
  8. EOFまで4を繰り返す

wavの場合, RIFFデータの先頭から数えて9〜12バイト目(上記処理でいうと3)のバイトの並びが 'W''A''V''E'となっているか否かで判定されます. ここが'A''V''I'' 'であればそれはaviファイルなので今回の対象外となります.

wavのサブチャンクで必須なものは以下の識別子を持つものです.

  • 'f''m''t'' '
  • 'd''a''t''a'

dataチャンクは波形データそのものであり, fmtチャンクはdataチャンクのデータの形式がどういったものであるかを定義しています. fmtチャンクはdataチャンクよりも前にくる必要があります. そうしないとシーケンシャルにデータを読んだ時にこのdataがどういう形式で格納されているのかわからなくなるからです.

このfmtチャンクはdataチャンクがどういったものであれ, 共通のフォーマットとして定義できるものと, 個々の形式によって定義が異なるものがあり, この内共通で定義できるものが, 先に参考資料として上げた"WAVEFORMAT"です.

WAVEFORMATは以下のような構造になっています.

typedef struct waveformat_tag {
  WORD  wFormatTag;
  WORD  nChannels;
  DWORD nSamplesPerSec;
  DWORD nAvgBytesPerSec;
  WORD  nBlockAlign;
} WAVEFORMAT;

これは先の参考資料に記載あるCの構造体です. Visual Studioに同梱されるmmreg.hにも同様の記載があります.

各要素は概ね以下の通りです.

wFormatTag
フォーマットタグ. dataチャンクに含まれるデータのフォーマットタイプを示します.
nChannels
チャンネル数. モノラルは1, ステレオは2
nSamplesPerSec
サンプリングレート
nAvgBytesPerSec
1秒あたりに要するバイト数. 16bit 48kHz ステレオの場合は48000*2*2=192kByte/sec.
nBlockAlign
ブロックサイズ. 1サンプルあたりの量子化ビット数のバイト表現 * チャンネル数. 16bit ステレオの場合は 2*2=4 Byte/sample.

基本的にはフォーマットタグでwavのフォーマットを判定し, そのフォーマットとチャンクサイズによってこの後取得するバイト列のサイズとデータ表現を判別します.

一般的な無圧縮PCMの場合, フォーマットタグは0x0001が設定される事になっています. これはmmreg.hに以下のように定義されています.

#define WAVE_FORMAT_PCM         1

他にもフォーマットタグとしては沢山の定義があり, mmreg.hには以下のような記載があります(これは定義の最初の部分のみです)

#define  WAVE_FORMAT_UNKNOWN                    0x0000 /* Microsoft Corporation */
#define  WAVE_FORMAT_ADPCM                      0x0002 /* Microsoft Corporation */
#define  WAVE_FORMAT_IEEE_FLOAT                 0x0003 /* Microsoft Corporation */
#define  WAVE_FORMAT_VSELP                      0x0004 /* Compaq Computer Corp. */
#define  WAVE_FORMAT_IBM_CVSD                   0x0005 /* IBM Corporation */
#define  WAVE_FORMAT_ALAW                       0x0006 /* Microsoft Corporation */
#define  WAVE_FORMAT_MULAW                      0x0007 /* Microsoft Corporation */
#define  WAVE_FORMAT_DTS                        0x0008 /* Microsoft Corporation */
#define  WAVE_FORMAT_DRM                        0x0009 /* Microsoft Corporation */
#define  WAVE_FORMAT_WMAVOICE9                  0x000A /* Microsoft Corporation */
#define  WAVE_FORMAT_WMAVOICE10                 0x000B /* Microsoft Corporation */

フォーマットタグがWAVE_FORMAT_PCMの場合, WAVEFORMATのデータ構造の次の2バイトは1サンプルあたりの量子化ビット数が入ります. Microsoftではこのデータまでを含めて以下のような構造体も定義しています.

typedef struct pcmwaveformat_tag {
    WAVEFORMAT  wf;
    WORD        wBitsPerSample;
} PCMWAVEFORMAT;

フォーマットタグ次第では, この先のデータに追加情報などが入っている場合があります. それらについてはそのフォーマットタグ毎に構造体や処理をわける必要があります. が, 今回はひとまず無圧縮PCMについてのみ考えていこうと思います.

とはいっても, 無圧縮PCMの場合, 後はこれまでと同様にFourCCとチャンクサイズを判定してdataチャンクを読み取り, fmtチャンクの情報を元に波形データを解釈すれば良いです.

という処で, 次回から実際にwavのデータを読み取る物を構築していこうと思います.