2019年8月2日金曜日

XAudio2SamplesのDirectX::LoadWAVAudioFromFileExを読み解く

入門XAudio2を読んでいて,45ページ目から48行目でサラッと流されているWAVファイルの読み込み処理,DirectX::LoadWAVAudioFromFileExについて気になったので読み解いてみようかと.

Qiitaに書くのは何か違うかな,と思ったのでこっちで.

RIFFについては知っている前提で.

RIFF(Resource Interchange File Format)についてはこちらを参照(Microsoft Docs)

DirectX::LoadWAVAudioFromFileEx関数で読み取った結果は,次のようなDirectX::WAVData構造体に保存されます.

namespace DirectX
{
    struct WAVData
    {
        const WAVEFORMATEX* wfx;
        const uint8_t* startAudio;
        uint32_t audioBytes;
        uint32_t loopStart;
        uint32_t loopLength;
        const uint32_t* seek;
        uint32_t seekCount;
    };
}

で,そのDirectX::LoadWAVAudioFromFileEx関数の宣言は次のような感じです.

namespace DirectX
{
    HRESULT LoadWAVAudioFromFileEx(
        _In_z_ const wchar_t* szFileName,
        _Inout std::unique_ptr& wavData,
        _Out_ WavData& result
    );
}

この,_In_z_とか_Inout_は,ソースコード注釈言語(Source-code Annotation Language, SAL)です.詳しくは,Microsoft Docsを検索してください.簡単に言うと,Visual Studioがコードの使い方に問題がないかチェックするための付加情報です.

szFileNameはファイルパス,wavDataにはファイルのデータ全てが格納されて,WAVData::startAudioはその中の音データの開始位置を指します.

さて,中を見ていきましょう.最初の数行は,ファイルパスへのポインタがNULLだったらエラー,出力先を0初期化,というだけなので飛ばします.



DWORD bytesRead = 0;
HRESULT hr = LoadAudioFromFile(szFileName, wavData, &bytesRead);

早速新しい関数が出てきました.


static HRESULT LoadAudioFromFile(
    _In_z_ const wchar_t* szFileName,
    _Inout_ std::unique_ptr& wavData,
    _Out_ DWORD* bytesRead
);

単純に,szFileNameのパスにあるファイルを開いて,wavDataにファイルの内容を書き込み,bytesReadに読み取ったバイト数を書き込む,というだけの関数です.

もう少し詳しく見てみると,Windows 8以降の場合は,CreateFile2を使ってファイルを開き,それより前の場合はCreateFileWを使ってファイルを開くようになっています.ちょっと調べてみたんですが,敢えて使い分ける理由がサッとは見つかりませんでした.

そして,Vista以降ならGetFileInformationByHandleExを使い,それより前の場合はGetFileSizeExを使ってファイルサイズを取得しています.これも,わざわざ使い分ける理由がいまいち不明です.GetFileInformationByHandleExの場合,エラー処理が入っているので,何らかの理由でファイルサイズがちゃんと取得できないケースがあるのかもしれません.

そして,このファイルサイズが32bitで表現できる範囲を超える場合や,WAVファイルとして最低限必要なRIFFチャンクと"fmt "チャンクのサイズ以下の場合,エラーとしています.

ここまで来ると,あとはwavDataに必要なサイズ分のメモリを割り当て,ファイルから読み取るだけです.この関数内で勝手にメモリを割り当てるからunique_ptrにしているんでしょうね.

さて,DirectX::LoadWAVAudioFromFileExに戻ります.


bool dpds, seek;
hr = WaveFindFormatAndData(
    wavData.get(),
    bytesRead,
    &result.wfx,
    &result.startAudio,
    &result.audioBytes,
    dpds,
    seek
);

またまた新しい関数です.


static HRESULT WaveFindFormatAndData(
    _In_reads_bytes_(wavDataSize) const uint8_t* wavData,
    _In_ size_t wavDataSize,
    _Outptr_ const WAVEFORMATEX** pwfx,
    _Outptr_ const uint8_t** pdata,
    _Out_ uint32_t* dataSize,
    _Out_ bool& dpds,
    _Out_ bool& seek
);


  • wavData
    • WAVファイルのデータを渡す
  • wavDataSize
    • wavDataのサイズを渡す
  • pwfx
    • wavData中のfmtチャンクから,WAVEFORMATEXの位置へのポインタを返す
  • pdata
    • wavData中のdataチャンクから,音データの開始位置へのポインタを返す
  • dataSize
    • pdataのサイズを返す
  • dpds
    • dpdsチャンクの有無を返す.dpdsチャンクはxWMAフォーマットの場合に存在する.
  • seek
    • Xbox OneでサポートされているXMA2フォーマットの場合にtrueになる,とコメントを読み解くと書いてある.

さて,この中で最初に呼ばれているFindChunkという関数は,次のような関数です.


static const RIFFChunk* FindChunk(
    _In_reads_bytes_(sizeBytes) const uint8_t* data,
    _In_ size_t sizeBytes,
    _In_ uint32_t tag
)

dataの先頭がRIFFのヘッダ(4バイトのfourccと4バイトのチャンクサイズ)と仮定して,sizeBytes先までにtagと一致するfourccを持つチャンクを探してくれるだけの関数です.

この関数を使って,まずはRIFFチャンクを探します.このRIFFチャンクがなかったり,RIFFチャンクのRIFFヘッダの直後のfourccがWAVEまたはXWMAで無い場合は正しいファイルでない,とエラーを返しています.

RIFFチャンクからRIFFヘッダとfourcc分のバイト数を進めた位置からサブチャンクが始まります.次に,このサブチャンクからfmtチャンクを探します.fmtチャンクには,少なくともWAVEFORMAT構造体のデータが含まれています.

このWAVEFORMAT構造体にはwFormatTagという音データの形式が格納されていて,PCM(pulse code modulation)形式や,ADPCMフォーマットである,といったことを確認し,その上でfmtチャンクのサイズに問題がないか,dpdsやseekは設定できるか,といったことをチェックしています.

fmtチャンクが終わったら,チャンクの終端に進み,dataチャンクを探し,サイズが渡されたデータを超えていないかチェックをしたら,dataチャンクのデータ部分の先頭をpdataに設定して,dataチャンクのサイズをdataSizeに設定して,この関数は終了です.

DirectX::LoadWAVAudioFromFileExに戻ると,次はWaveFindLoopInfo関数です.



static HRESULT WaveFindLoopInfo(
    _In_reads_bytes_(wavDataSize) const uint8_t* wavData,
    _In_ size_t wavDataSize,
    _Out_ uint32_t* pLoopStart,
    _Out_ uint32_t* pLoopLength
);

wavData,wavDataSizeはWaveFindFormatAndDataと同じなのでスキップします.この関数は,WAVファイルからループの開始サンプル数とループの長さのサンプル数を,それぞれpLoopStartとpLoopLengthが指す先に格納してくれます.

この中では,RIFFチャンクがWAVEかどうか確認しています.xWMAはループ情報を含まないようです.そして,DLSチャンク(fourccはwsmp)またはSampleチャンク(fourccはsmpl)からループ情報を取り出しています.

DLSは,Downloadable Soundの略っぽいですが,該当するデータが手元に無いので詳細は不明です.

smplチャンクは,コード中ではMIDIチャンクと書いているので,MIDIで作成した音しかループ情報を含んでいないのでしょうか?

このループ情報の取り出しは,場合によってはスキップしても良いかもしれません.

DirectX::LoadWAVAudioFromFileExに戻ると,dpdsやseekがtrueかどうかで次のような関数を呼び出して,必要なデータを取り出しています.


static HRESULT WaveFindTable(
    _In_reads_bytes_(wavDataSize) const uint8_t* wavData,
    _In_ size_t wavDataSize,
    _In_ uint32_t tag,
    _Outptr_result_maybenull_ const uint32_t** pData,
    _Out_ uint32_t* dataCount
);


  • wavData,wavDataSize
    • おなじみWAVファイルの中身とサイズ
  • tag
    • RIFFチャンクのサブチャンクから抜き出すチャンクのfourcc
    • dpdsの場合,dpdsチャンク,seekの場合seekチャンクを指定
  • pData
    • tagで指定したチャンクのデータ部分へのポインタを返す
    • 4バイトのデータの配列になっている
  • dataCount
    • tagで指定したチャンク内にある4バイトのデータの数
これでようやく終わりです.

この関数は,ファイルデータをすべてメモリに載せた上での処理になっています.しかし実際には,効果音などの短い音であればメモリに載せられても,BGMなどのようにある程度のサイズがあるデータは,必要な部分を徐々に読み取って再生するのが一般的だと思うので,そういった場合は,FindChunkのように都度必要なチャンクを探して回るのではなく,チャンクを発見する度に必要な情報を埋めていって,といった形にする必要があるかもしれません.