mmioを使用してWAVファイルを読み込む

概要

最終更新日:2020/03/06

mmioを使用したWAVファイルの扱い方について書いた記事です。
この記事は以下の内容を知りたい方に向けて書いています。
  • WAVファイルの構成が知りたい
  • WAVファイルの読み書きの仕方が知りたい
  • mmio関数のことが知りたい

サンプル

サンプルはこちらからダウンロードできます。
開発環境については以下の内容です。

開発環境
VSのバージョン VisualStudio 2019
DirectXのバージョン DirectX9
説明 サウンドの再生のためDirectSoundを使っています。


WAVファイルとは

WAVファイルとはサウンドファイルのフォーマットの一つです。
mmioを使用してWAVファイルの操作を行うためにファイル構成の説明をします。

ファイル構成

WAVファイルはRIFF形式と呼ばれる方法で構成されており、
チャンクと呼ばれるデータの固まりが複数連結しています。
下の図は構成イメージです。

directx_0174

本来はfmtチャンクやdataチャンク以外にもチャンクが存在しますが、
今回の図では省略しています。

チャンクのデータ構成

RIFFやfmt、data等のチャンクはデータ構成が決まっています。

directx_0171

データの「riff」や「id」は各チャンクを識別するために使用される情報で、
「size」はファイルサイズやチャンクサイズを表しています。
このように各チャンクにどのようなデータが保存されているかを知っておけば
ファイルを読み込む際に、どのチャンクを読み込むべきかが分かります。

WAVファイルの操作方法

WAVファイルの操作はWinAPIのmmio(Multi Media IO)を使用して行います。
mmioはRIFF形式のファイルを操作する関数が用意されています。
以下の項目はファイルの読み書きで使用するファイル操作の一覧です。
  1. ファイルのオープンとクローズ
  2. チャンクへの進入と退出
  3. チャンクの読み込み
  4. チャンクの書き込み

ファイルのオープンとクローズ

WAVファイルも通常のファイルと同様にオープンさせてから、
ファイルに対する処理を行い、用が済んだらクローズします。
ファイルのオープンは「mmioOpen関数」クローズは「mmioClose関数」を使用します。

mmioOpen

mmioOpen関数は引数でファイル名とモードの指定を行い、
その結果、MMIOの情報とHMMIOを取得できます。
HMMIOはMMIOファイルのハンドルです。

directx_0172
// WAVファイルを開く
mmio_handle = mmioOpen(
	(char*)file_name,	// ファイル名
	NULL,			// MMIO情報
	MMIO_READ);		// モード

if (mmio_handle == NULL)
{
	// オープン失敗
	return false;
}

モードで必ず設定する項目は次の三種類です。
  • 読み込み専用:MMIO_READ
  • 書き込み専用:MMIO_WRITE
  • 読み書き可:MMIO_READWRITE
上の項目以外にもオプションの設定はありますが、
まずはこの三種類で開いた後のファイルの扱い方を決めます。

mmioClose

mmioClose関数は引数にクローズするハンドルを指定します。
この関数はmmio関数より後で行ったmmio関連の処理がエラーになる、
または関数が終了する前に必ず実行してください。

directx_0181
// WAVファイルを閉じる
mmioClose(
	mmio_handle,	// クローズ対象のハンドル
	MMIO_FHOPEN);	// オプション(設定なしは0)

第二引数で指定している「MMIO_FHOPEN」はファイルチェックのオプションです。
このオプションは第一引数の値がmmioハンドルだった場合はクローズを行いますが、
もし、mmioハンドルではなかった場合は、何もしないという設定になります。

チャンクへの進入と退出

WAVファイルはチャンクごとにデータが分かれているため、
アクセスするチャンクを決める必要があります。
このアクセスするチャンクを決めることを、ここでは「進入」と表現しています。

directx_0182

そして、別のチャンクにアクセスをしたくなった場合は、
選択中のチャンクをキャンセルしなくてはいけません。
この選択中のチャンクをキャンセルすることを、ここでは「退出」と表現しています。

directx_0183

ただ、階層として次のチャンクが下(子)ならばそのまま進入を続けることができます。
例えば読み込みをする際は必ずRIFFチャンクへの進入から始まりますが、
進入後、別のチャンクへ進入する際にRIFFチャンクから退出することはありません。
これはRIFFが階層の一番上にあるため、退出する必要がないこと示しています。

チャンクへの進入は「mmioDescend関数」退出は「mmioAscend関数」で行います。

mmioDescend

mmioDescend関数にはMMIOハンドルと進入するチャンク種類などの情報を渡すことで、
該当するチャンクとその親チャンクの情報を取得できます。

directx_0173
// dataフォーマットへの進入設定
ck_info.ckid = mmioFOURCC('d', 'a', 't', 'a');

// dataチャンクに進入する
if (MMSYSERR_NOERROR != mmioDescend(
	mmio_handle,	// MMIOハンドル
	&riffck_info,	// 進入したチャンクの情報
	&riffck_info,	// 親チャンク
	MMIO_FINDCHUNK))// 情報の種類
{
	// 進入失敗
	return false;
}

mmioDescendの解説の前にmmioFOURCCについて説明します。
mmioFOURCCはマクロになっており、引数に4つの文字を指定できます。
この文字をビットシフトでずらして、4バイトに1文字ずつ文字が配置された
整数を作成しています。

directx_0184

このマクロによって、WAVフォーマットのckidやfccTypeの
文字が設定された整数を簡単に作成できるようになります。

それではmmioDescend関数ですが、この関数は少しややこしい所があります。
それは第二引数は出力用の引数だけでなく、入力用の引数も兼ねていることです。
mmioDescend関数は第二引数のMMCKINFOの「ckid」または「fccType」と
第四引数の情報の種類の組み合わせで進入するチャンクを探しています。
組み合わせは以下の通りです。

検索対象のチャンク MMCKINFOの変数名 MMCKINFOの値 情報の種類
RIFF fccType WAVE MMIO_FINDRIFF
その他のチャンク ckid 各チャンクのid MMIO_FINDCHUNK
上のコードでは、dataフォーマットに進入しようとしているので、 MMCKINFOのckidにckidの「data」を設定して、 mmioDescend関数の第四引数にはMMIO_FINDCHUNKを指定しています。 この設定により該当するチャンクが検索され、無事見つかったら進入します。 進入できたら第二、第三引数にチャンクの情報が保存されます。 このように第二引数が入出力兼用になっているので、注意してください。

mmioAscend

mmioAscend関数にはMMIOハンドルと進入中のチャンク情報を渡すことで、
チャンクから退出することができます。

directx_0177
// dataチャンクを退出する
if (mmioAscend(
	mmio_handle, 		// MMIOハンドル
	&ck_info, 		// 進入中のチャンク
	0)			// 0固定
	!= MMSYSERR_NOERROR)
{
	// 退出失敗
	return false;
}

mmioAscend関数は先ほどのmmioDescendとは異なり非常にシンプルです。
ハンドルと進入中のチャンクを指定すればチャンクから退出できます。
これで、別のチャンクに進入することができるようになります。

チャンクの読み込み

チャンクデータの読み込みは進入中のチャンクに対してのみ行うことができます。
関数は「mmioRead関数」を使用します。

mmioOpenで読み書きの設定をしていますが、この設定に反した処理はできません。
例えば、読み込み専用でファイルを開いている場合は
書き込みができないということです。

mmioRead関数

mmioRead関数は引数にハンドルと読み込み内容を保存するバッファ、
バッファのサイズを指定します。

directx_0175
// チャンクデータの読み込み
mmioRead(
	mmio_handle, 	// ハンドル
	(char*)buffer, 	// 読み込み先のバッファ
	buffer_size);	// バッファのサイズ

第二引数のバッファはHPSTR型となっていますが、これはchar*をtypedefした型です。
第三引数のバッファのサイズは読み込みサイズも兼ねています。

チャンクの書き込み

チャンクデータに書き込むのはチャンクに進入していなくても可能です。
関数は「mmioWrite関数」を使用します。
書き込みもmmioOpenで書き込み設定をしていないと書き込めません。

mmioWrite関数

mmioWrite関数はハンドルと書き込みデータ、書き込みサイズを指定します。

directx_0185
// チャンクデータの書き込み
mmioWrite(
	mmio_handle, 	// ハンドル
	buffer,	 	// 書き込みバッファ
	buffer_size);	// バッファのサイズ

ファイルの読み込み方法

WAVファイルの読み込みは以下の手順で行います。
  1. ファイルを開く
  2. RIFFチャンクへ進入する
  3. チャンクへ進入する
  4. チャンクで読み書きをする
  5. チャンクから退出する
  6. ③~⑤を繰り返す
  7. ファイルを閉じる
    ※②~⑥までで失敗しても行う

ファイル読み込み実装

上の手順でWAVファイル読み込みを行います。
※提示するコードの中には先ほどの「WAVファイル操作の流れ」と
 同じ内容がありますがご了承ください。

まず、ファイルを開きます。

// WAVファイルを開く
mmio_handle = mmioOpen(
	(char*)file_name,	// ファイル名
	NULL,			// MMIO情報
	MMIO_READ);		// オープンモード

if (mmio_handle == NULL)
{
	// オープン失敗
	return false;
}

次はWAVファイルの先頭チャンクであるRIFFチャンクに進入します。

// RIFFチャンクに進入するためにfccTypeにWAVEを設定する
riffck_info.fccType = mmioFOURCC('W', 'A', 'V', 'E');

// RIFFチャンクに進入する
if (MMSYSERR_NOERROR != mmioDescend(
	mmio_handle,	// MMIOハンドル
	&riffck_info,	// 取得したチャンクの情報
	NULL,		// 親チャンク
	MMIO_FINDRIFF))	// 取得情報の種類
{
	// 失敗
	mmioClose(mmio_handle, MMIO_FHOPEN);
	return false;
}

進入が成功したら、各チャンクに進入していきます。
最初はWAVファイルのサンプリングレートなどの情報を保持している
「fmtチャンク」に進入します。

// 進入先のチャンクを"fmt "として設定する
ck_info.ckid = mmioFOURCC('f', 'm', 't', ' ');
if (MMSYSERR_NOERROR != mmioDescend(mmio_handle, &ck_info, &riffck_info, MMIO_FINDCHUNK))
{
	// fmtチャンクがない
	mmioClose(mmio_handle, MMIO_FHOPEN);
	return false;
}

進入が完了したら、fmtチャンクの情報を読み込みます。
読み込み用のバッファはfmtチャンクデータ構成用にPCMWAVEFORMAT構造体が
用意されているので、こちらを使用すると取得後が楽になります。

// fmtデータの読み込み
LONG read_size = mmioRead(
		mmio_handle,			// ハンドル
		(HPSTR)&pcm_wave_format,	// 読み込み用バッファ
		sizeof(pcm_wave_format));	// バッファサイズ

if (read_size != sizeof(pcm_wave_format))
{
	// 読み込みサイズが一致してないのでエラー
	mmioClose(mmio_handle, MMIO_FHOPEN);
	return false;
}

読み込みが無事終了したかどうかの判断はmmioReadの読み込みサイズで行います。
mmioReadは第三引数のバッファのサイズ分を読み込んでくれるので、
今回のように構造体のサイズを指定した場合は、戻り値の読み込んだサイズが
バッファサイズよりも少ないとデータに問題がある可能性があると考えてください。

読み込みが完了したら次のチャンクに移動するため、fmtチャンクを退出します。

// fmtチャンクを退出する
if (mmioAscend(mmio_handle, &ck_info, 0) != MMSYSERR_NOERROR)
{
	mmioClose(mmio_handle, 0);
	return false;
}

退出したら次は音データを保持しているdataチャンクに進入します。

// dataチャンクに進入する
ck_info.ckid = mmioFOURCC('d', 'a', 't', 'a');
if (mmioDescend(mmio_handle, &ck_info, &riffck_info, MMIO_FINDCHUNK) != MMSYSERR_NOERROR)
{
	// 進入失敗
	mmioClose(mmio_handle, MMIO_FHOPEN);
	return false;
}

進入が成功したら音データを読み込みます。
この時のバッファのサイズは先ほどのfmtフォーマットのPCMWAVEFORMAT構造体の
メンバ変数である「cksize」が音データのサイズを保存しているので、
この変数を使用します。

// 音データの読み込み
char* sound_buffer = new char[ck_info.cksize];
read_size = mmioRead(mmio_handle, (HPSTR)sound_buffer, ck_info.cksize);
if (read_size != ck_info.cksize)
{
	mmioClose(mmio_handle, MMIO_FHOPEN);
	delete[] sound_buffer;
	return false;
}

データの読み込みが完了したら、このファイルで行う処理は
全て完了したのでファイルを閉じます。

// ファイルを閉じる
mmioClose(mmio_handle, MMIO_FHOPEN);

これで、WAVファイルの読み込みが完了したので、
読みんだデータを有効に使用してDirectSound等によるサウンド再生に繋げます。