入門XAudio2を読んでいて,45ページ目から48行目でサラッと流されているWAVファイルの読み込み処理,DirectX::LoadWAVAudioFromFileExについて気になったので読み解いてみようかと.
Qiitaに書くのは何か違うかな,と思ったのでこっちで.
RIFFについては知っている前提で.
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のように都度必要なチャンクを探して回るのではなく,チャンクを発見する度に必要な情報を埋めていって,といった形にする必要があるかもしれません.
0 件のコメント:
コメントを投稿