WAVEのマルチバッファリング

概要:途切れる事なく再生を繰り返す

再生終了を通知する MM_WOM_DONE メッセージ処理で
次のWAVEを再生させても一瞬だけ無音が発生してしまいます。
本節では一瞬の無音さえ無い連続再生を可能にする手法を解説します。

■マルチバッファリング

waveOutWrite関数はWAVEバッファを再生デバイスの再生待ちキューに書き込みます(プッシュします)。
再生が終了すればバッファはアプリケーションに返されます。
ここで再生が終了する前に更にwaveOutWrite関数を呼び出せば、
バッファはキューに蓄積され、順番に処理されるのを待つ事になります。
現在の再生を中止して、新しい再生を開始したりはしません。
そして、もしキューが空になってしまったら無音が発生します。
従って、キューが空になる前に新しいバッファを書き込む事ができれば、
永遠に途切れる事のない音を実現できます。
その為にはバッファが複数必要であり、この手法をWAVEのマルチバッファリングといいます。

それでは、同じWAVEを途切れる事なく繰り返し再生させる方法を考えてみましょう。
ただし、再生中のバッファに対してwaveOutWrite関数を呼び出す事はできません。
この時、waveOutWrite関数が返すエラーコードをmciGetErrorString関数に渡すと次の文字列が得られます。
 メディアデータの再生中にこの操作を実行することはできません。
 デバイスをリセットするか、またはデータの再生が終了するまで待ってください。
従って、コピーのバッファを用意して最初は両方、
一つ目の再生が終了してからは交互にwaveOutWrite関数を呼び出す事で、常にバッファを供給し続けます。

これを実装する場合は、再生を開始する前に二つのバッファの準備をしておきます。

#define SRATE    8000    //標本化周波数(1秒間のサンプル数)
#define F        400     //周波数(1秒間の波形数)

WAVEFORMATEX wfe;
static HWAVEOUT hWaveOut;
static WAVEHDR whdr[2];
static LPBYTE lpWave[2];
int i,len,k;

wfe.wFormatTag=WAVE_FORMAT_PCM;
wfe.nChannels=1;    //モノラル
wfe.wBitsPerSample=8;    //量子化ビット数
wfe.nBlockAlign=wfe.nChannels * wfe.wBitsPerSample/8;
wfe.nSamplesPerSec=SRATE;    //標本化周波数
wfe.nAvgBytesPerSec=wfe.nSamplesPerSec * wfe.nBlockAlign;

waveOutOpen(&hWaveOut,WAVE_MAPPER,&wfe,(DWORD)hWnd,0,CALLBACK_WINDOW);

len=SRATE/F;    //波長
for(k=0;k<2;k++){
    lpWave[k]=(LPBYTE)calloc(wfe.nAvgBytesPerSec,2);    //2秒分

    for(i=0;i<SRATE*2;i++){  //波形データ作成
        if(i%len < len/2)    lpWave[k][i]=128+64;
        else                 lpWave[k][i]=128-64;
    }

    whdr[k].lpData=(LPSTR)lpWave[k];
    whdr[k].dwBufferLength=wfe.nAvgBytesPerSec * 2;
    whdr[k].dwFlags=WHDR_BEGINLOOP | WHDR_ENDLOOP;
    whdr[k].dwLoops=1;

    waveOutPrepareHeader(hWaveOut,&whdr[k],sizeof(WAVEHDR));
}

属性を格納するWAVEFORMATEX構造体は流用できますが、
バッファを指すポインタを格納するWAVEHDR構造体は各々に必要です。

また、バッファの書き込みは一つの再生デバイスに対して行われるので、
再生デバイスは一つだけオープンします。

準備が完了したら、再生する事ができます。
最初の再生では二つのバッファを両方とも書き込みます。
一つのバッファの再生が終了した時に発生する MM_WOM_DONE メッセージを処理している間にも
もう一方のバッファを再生させ、無音を回避する為です。

for(k=0;k<2;k++){
    waveOutWrite(hWaveOut,&whdr[k],sizeof(WAVEHDR));
    whdr[k].dwUser=0;
}

ところで、WAVEHDR構造体のdwLoopsメンバはループ再生させる回数ですが、
これはマルチバッファリングとは少し違うようです(私の推測)。
というのも、dwLoopsで指定した回数だけ再生し終えるまで
MM_WOM_DONE メッセージが発生しない
からです。
内部的にバッファを複製しているのであれば、個々のバッファの再生が終了した時点で
MM_WOM_DONE メッセージが発生するはずです。
従って、dwLoopsは本当の意味での再生回数なのかもしれません。
どうやって実現しているかは不明ですけどね……。
可能性とはしては、バッファのサイズをdwLoops倍して同じデータを書き込んだとか……?
ちなみに、dwLoopsメンバで指定した回数だけ再生している間は無音は発生しません

whdr[k].dwUser=0; はdwUserメンバを再生終了の原因を示すフラグとして利用しています。
0 なら最後まで再生した、
1 ならwaveOutReset関数で停止した事を表すようにします。
dwUserメンバはユーザーが自由に使う事ができます。

何故dwUserメンバを使う必要があるのかといえば、
最後まで再生した場合もwaveOutReset関数で停止した場合も
MM_WOM_DONE メッセージが発生し、また他の情報も全く同じになり、区別できないからです。

waveOutReset(hWaveOut);
for(k=0;k<2;k++){
    whdr[k].dwUser=1;    //waveOutReset関数を呼び出した
}

waveOutReset関数は全てのバッファをアプリケーションに返します。
まだ再生していないバッファも返します。

再生が終了したら MM_WOM_DONE メッセージが発生します。
WPARAM には再生終了した HWAVEOUT が、
LPARAM には再生終了した LPWAVEHDR が格納されています。

最後まで再生されて停止した場合は
LPARAM と全てのWAVEHDR構造体のポインタを比較する事で、再生終了したバッファを検索します。
見つかったバッファは再生を終了したバッファなので、
再生中のバッファは一つ後ろのバッファという事になります。

waveOutReset関数で停止した場合は
全てのバッファがアプリケーションに返されるので、
もう一度再生を開始する時は 0 番のバッファから再生される事になります(そのように実装している)。

static int buf_num;

case MM_WOM_DONE:
    if(((LPWAVEHDR)lParam)->dwUser){    //waveOutReset関数で停止した
        buf_num=0;    //初期値に戻す
    }else{        //最後まで再生した
        waveOutWrite((HWAVEOUT)wParam,(LPWAVEHDR)lParam,sizeof(WAVEHDR));
        for(k=0;k<2;k++){
            if((LPWAVEHDR)lParam==&whdr[k]){
                buf_num=k;    //再生を終了したバッファ
                break;
            }
        }
        buf_num=(buf_num+1)%2;    //再生中のバッファ
    }
    InvalidateRect(hWnd,NULL,TRUE);
    return 0;

描画と終了の処理は特に解説する事がありません。

HDC hdc;
PAINTSTRUCT ps;
char str[32];

case WM_PAINT:
    hdc=BeginPaint(hWnd,&ps);
    wsprintf(str,"再生中のバッファ = %d",buf_num);
    TextOut(hdc,0,0,str,lstrlen(str));
    EndPaint(hWnd,&ps);
    return 0;
case WM_DESTROY:
    waveOutReset(hWaveOut);
    for(k=0;k<2;k++){
        waveOutUnprepareHeader(hWaveOut,&whdr[k],sizeof(WAVEHDR));
    }
    waveOutClose(hWaveOut);
    for(k=0;k<2;k++){
        free(lpWave[k]);
    }
    PostQuitMessage(0);
    return 0;

以上でマルチバッファリングの実装は完了です。

今回はバッファのコピーを用意する事でマルチバッファリングを実現しましたが、
メモリ効率を重視するならバッファを半分に分割して、交互に書き込む方が良いでしょう。
ただし、バッファサイズを計算しなければならず、
マルチバッファリングが不要な場合でも二つのバッファを書き込まなければならないので、
使い勝手は悪くなります。
必要に応じて二つの方法を使い分けて下さい。

また、マルチバッファリングですから、
二つに限らず、三つでも四つでもバッファを書き込む事はできます。
しかし、今回のような特定のWAVEの繰り返しではトリプルバッファリング以上は意味がありません。

ところで、再生中のバッファと違うバッファを書き込めば、それは新しく書き込んだバッファの再生予約をした事になります。

★☆ ソースファイル表示 ☆★

■WAVEHDR構造体の詳細

typedef struct {
  LPSTR lpData;
  DWORD dwBufferLength;
  DWORD dwBytesRecorded;
  DWORD_PTR dwUser;
  DWORD dwFlags;
  DWORD dwLoops;
  struct wavehdr_tag * lpNext;
  DWORD_PTR reserved;
} WAVEHDR;


解説していなかったメンバについて解説します。

dwBytesRecorded は録音されたバッファのバイト数です。
 waveInStart関数の結果として設定されます。
dwUser はユーザーデータであり、自由に使う事ができます。
dwFlags はループ情報を設定したり、バッファ情報が設定されたりするフラグです。

WHDR_BEGINLOOP 0x00000004 このバッファはループの最初のバッファです
WHDR_DONE 0x00000001 再生が終了しました。
最後まで再生したか、waveOutReset関数を呼び出した時に設定され、
waveOutWrite関数を呼び出した時に解除される
WHDR_ENDLOOP 0x00000008 このバッファはループの最後のバッファです
WHDR_INQUEUE 0x00000010 キューに蓄積されています。
waveOutWrite関数を呼び出した時に設定され、
再生が終了した時に解除される
WHDR_PREPARED 0x00000002 バッファが準備されました。
waveOutPrepareHeader関数を呼び出した時に設定され、
waveOutUnprepareHeader関数を呼び出された時に解除される

WHDR_BEGINLOOP または WHDR_ENDLOOP を指定した場合は
必ずもう一方を別のバッファに設定して下さい。
バッファ全部で最初と最後が設定されていない場合はおかしな事になります。

waveOutWrite関数を呼び出す段階では WHDR_INQUEUE フラグと WHDR_DONE フラグが交互に設定される事になります。
これらのフラグを調べれば、再生が終了しているか等を判定する事ができます。

dwLoops はループ再生する回数です。0 を指定しても 1 に同じです。
lpNext と reserved は予約されています。


戻る / ホーム