カクタスソフトウェア
カクタスソフトウェア
サウンド MIDI マルチメディア アプリケーション

バッファ更新のメカニズム

ストリーム再生は、セカンダリバッファをリング状にして運用し、エンドレスでループ再生するという手法を取ります。セカンダリーバッファを更新するために、定期的にサービス関数を呼び出します。定期的なサービスを行う方法については、セクション「タイミングを作る」を参照して下さい。

リングバッファによる運用

サイズの大きいウェーブデータを再生する場合、すべてのデータをメモリに保存しておくことは効率的ではありません。通常、ウェーブ本体はハードディスクに保存し、再生に必要な部分のみをメモリに読み込む「ストリーミング」手法を用います。

セカンダリーバッファをストリーミングで用いるには、以下の図で示すようにバッファをリング状に配置し、書き込み位置と読み出し位置が追いかけっこをしながらぐるぐる回るイメージで運用します。これをリングバッファと呼びます。

読み出し位置から書き込み位置まで、図では黄色の部分が有効なデータが保存された領域です。この領域を変更禁止にすることで書き込まれたデータが正しく読み出されることを保障します。一方、反対側の青色の部分は、次の読み出しに備えてデータを更新できる領域です。

セカンダリーバッファのストリーミングでは、書き込み位置はサウンドカードへウェーブデータを転送するポイント、読み出し位置は今まさにウェーブデータが再生されているポイントに対応します。そして黄色の領域は、すでにサウンドデバイスへ転送された部分です。この領域を更新してももはやサウンドデバイスへは転送されないため、サウンド出力には反映されません。

ここでバッファ全体をN等分し、一回の処理で更新を行う領域を定義します。バッファのつなぎ目をまたぐ処理は、アドレス計算が煩雑になるため、更新領域の境界とリングバッファのつなぎ目が一致するよう工夫します。以下の図は、バッファを4等分した例です。データ更新は1領域に対して行い、この例では4回でバッファ全領域を更新したことになります。

以下に実際に運用中のバッファの様子を示します。

ここで重要なことは、更新領域は保護された領域と重なってはいけないということです。つまり以下の条件を満たす必要があります。

再生位置・書き込み位置の取得には、IDirectSoundBuffer8::GetCurrentPosition()関数を用います。

HRESULT GetCurrentPosition(
    LPDWORD     pdwCurrentPlayCursor,
    LPDWORD     pdwCurrentWriteCursor)

dwCurrentPlayCursorにサウンドの再生位置が、dwCurrentWriteCursorにサウンドカードへの書き込み位置が戻ります。これらの数値はバッファのつなぎ目からのオフセットであり、単位はバイトです。

ここで次の変数と判定関数を用意します。

DWORD m_dwWriteOffset;      // 更新開始位置
DWORD m_dwPageSize;         // 更新領域サイズ
DWORD m_dwBufferSize;       // 全バッファサイズ
hr = m_pDSBuffer->GetCurrentPosition(&dwCurrentPlayCursor, &dwCurrentWriteCursor);
if (FAILED(hr))             return hr;

DWORD dwCheckWriteCursor;

if (dwCurrentPlayCursor <= dwCurrentWriteCursor)
    dwCheckWriteCursor = dwCurrentWriteCursor;
else
    dwCheckWriteCursor = dwCurrentWriteCursor + m_dwBufferSize;

BOOL bCheck = CheckPositionSafe(
    dwCurrentPlayCursor,
    dwCheckWriteCursor,
    m_dwWriteOffset,
    m_dwWriteOffset + m_dwPageSize);
保護された領域にバッファのつなぎ目が含まれる場合とそうでない場合で計算式が異なることに注意して下さい。

次の関数は、4つのオフセットが正しい位置関係にあるかどうかを判定します。領域が不正の場合、関数はFALSEを戻します。

BOOL CheckPositionSafe(DWORD from0, DWORD thru0, DWORD from1, DWORD thru1)
{
    return (thru0 < from1) ^ (thru1 < from0);
}

適当な時間間隔でCheckPositionSafe()関数をコールし、TRUEが戻ったらバッファの更新処理を行います。

サービス関数の呼び出し

セカンダリーバッファを更新するために定期的にサービス関数を呼び出します。タイミングの作成はいくつかの方法があり、状況に応じて適当な方法を選びます。詳しい解説はセクション「タイミングを作る」を参照して下さい。

WM_TIMERイベントを用いる

メインプログラムがウィンドウを持っている場合、SetTimer()関数を用いてWM_TIMERイベントを発生させます。

サブスレッドでSleep()を用いる

作業用スレッドを作成し、内部に永久ループを作成します。Sleep()関数を用いて適当な時間間隔を作成します。

サブスレッドでタイマーを用いる

作業用スレッドを作成し、イベント待ちループを作成します。タイマーで定期的にイベントをシグナルにすることで時間間隔を作成します。

セカンダリーバッファの設計

一回の更新で処理するデータサイズは、その間に消費されるデータサイズより大きくなければなりません。もし小さいと音切れ(アンダーフロー)が生じます。以下にサウンドバッファのサイズと更新処理のインターバルを決める2つの方法を紹介します。

固定サイズ

以下の方針でバッファを設計します。

バッファサイズを1秒にすることで、どんなサンプリング周波数・チャンネル数にも対応可能です。また更新領域の演奏時間が分かっているので、処理インターバルも固定の値にすることができます。

任意サイズ

更新領域のサイズを任意の大きさにするには、以下のようにします。

    DWORD dwBufferSize = 希望サイズ;
    dwBufferSize -= dwBufferSize % wfx->nBlockAlign;

以下の式で更新処理のインターバル(mSec)が求まります。更新頻度は1000未満の適当な値。

    DWORD dwInterval = dwBufferSize * 更新頻度 / wfx->nAvgBytesPerSec;

バッファサイズが小さい場合、それに合わせてバッファ数を多く取ります。

原理的には更新領域は2枚あれば良く、処理インターバルも領域の演奏時間より短ければ動作します。筆者の実験では、1秒固定サイズ、更新領域2枚、処理インターバル980mSecで動作しました。

しかし処理スレッドが何らかの理由でブロックされた場合、バッファがあふれるかも知れません。そこで安全のために、最低でも2秒、できれば4秒程度のデータが収納できるようにします。また処理インターバルを領域の演奏時間の50%前後(参考値)にします。

バッファサイズをあまり小さくするとオーバーヘッドが大きくなるため効率的ではありません。

セカンダリーバッファの初期化

読み出し位置の初期化

スタートコマンドを実行したとき、セカンダリーバッファのどの位置から再生を始めるのかを設定します。バッファの初期化を行ったときや、巻き戻しコマンドを実行した場合など、通常ゼロを設定します。ポーズコマンドで一時停止した場合は、カレント位置を保存するため設定コマンドは実行しません。設定はバッファ先頭からのオフセットで、単位はバイトです。

m_pDSBuffer->SetCurrentPosition(0);

サウンドデータのロード

演奏に先立ってセカンダリーバッファにウェーブファイルの先頭部分をロードします。

バッファの全領域をロックします。サウンドバッファの内容を更新するには、更新したい部分をIDirectSoundBuffer8::Lock()関数でロックしなければなりません。ロックしたい開始オフセットとサイズをリクエストするとバッファのアドレスとサイズが戻ります。

LPVOID  pDSLockedBuffer = NULL;
DWORD   dwDSLockedBufferSize;

    HRESULT hr = m_pDSBuffer->Lock(
        0,
        dwBufferSize,
        &pDSLockedBuffer,
        &dwDSLockedBufferSize,
        NULL,
        NULL,
        0L);
    if (FAILED(hr))         return hr;

ロックが成功したら、ウェーブデータを先頭からを読み込みます。

    waveInstance.Rewind();
    long lReadSize = waveInstance.ReadData(pDSLockedBuffer, dwDSLockedBufferSize);
ここで用いているwaveInstanceクラスは、ウェーブファイルをカプセル化するサービスクラスです。詳細についてはセクション「ウェーブデータの供給」を参照して下さい。

セカンダリーバッファは、アロケートされた時点で初期化されていません。そのためウェーブデータのサイズがバッファサイズより小さい場合、残りの部分を無音データで埋めなければなりません。

無音の値は必ずしもゼロではありません。ウェーブフォーマットのwBitsPerSampleが8Bitの場合、128となります。

    if (lReadSize < 0)      return E_FAIL;

    DWORD dwWavDataRead = lReadSize;

    if (dwWavDataRead < dwDSLockedBufferSize)
    {
        waveInstance.FillSilence(
            (BYTE*)pDSLockedBuffer + dwWavDataRead, dwDSLockedBufferSize - dwWavDataRead);
    }

バッファにサウンドデータをセットしたら、IDirectSoundBuffer8::Unlock()関数でアンロックします。アンロックはロックで戻ったパラメータをそのまま指定します。

    m_pDSBuffer->Unlock(pDSLockedBuffer, dwDSLockedBufferSize, NULL, 0);

初期化が終わったら演奏を開始し、後は定期的にセカンダリーバッファの更新処理を行います。

セカンダリーバッファの更新

サウンドデータを更新領域にロードし、次の再生に備えます。セカンダリーバッファの更新に先立って、今回更新する領域のオフセットと更新サイズでバッファをロックします。

LPVOID  pDSLockedBuffer = NULL;
LPVOID  pDSLockedBuffer2 = NULL;
DWORD   dwDSLockedBufferSize;
DWORD   dwDSLockedBufferSize2;

    HRESULT hr = m_pDSBuffer->Lock(
        dwWriteOffset,
        dwPageSize,
        &pDSLockedBuffer,
        &dwDSLockedBufferSize,
        &pDSLockedBuffer2,
        &dwDSLockedBufferSize2,
        0L);
    if (FAILED(hr))         return hr;
ここでは更新領域の境界はバッファのつなぎ目と一致するよう配置しているので、pDSLockedBuffer2に有効なアドレスが戻ることはありません。

ロックが成功したら、ウェーブデータを読み込みます。

    long lReadSize = waveInstance.ReadData(pDSLockedBuffer, dwDSLockedBufferSize);

すべての領域がサウンドデータで満たされれば、更新は終了です。しかし、ウェーブファイルが終わりに達した場合など、更新できない領域が生じたときは、残りの部分を無音データで埋めなければなりません。古いデータをそのまま放置すると、再生されたときノイズとなります。またサウンドデータの読み出しに失敗した場合も、更新領域を無音データで埋めておく必要があります。

サウンドバッファの更新が終了したら、バッファをアンロックします。

    m_pDSBuffer->Unlock(pDSLockedBuffer, dwDSLockedBufferSize, NULL, 0);

そして次の更新に備え、書き込みオフセットを進めます。リングバッファでの運用なので、バッファサイズで割ったあまりを新しいオフセットとします。

    m_dwWriteOffset = (m_dwWriteOffset + m_dwPageSize) % m_dwBufferSize;

最後に今回の更新でどれだけ演奏が進んだかを計算します。

    DWORD dwPlayDelta;

    if (dwCurrentPlayCursor < m_dwLastPlayCursor)
        dwPlayDelta = (m_dwBufferSize - m_dwLastPlayCursor) + dwCurrentPlayCursor;
    else
        dwPlayDelta = dwCurrentPlayCursor - m_dwLastPlayCursor;

    m_dwPlayProgress += dwPlayDelta;
    m_dwLastPlayCursor = dwCurrentPlayCursor;

各パラメータの説明は「再生処理の流れ」を参照して下さい。

セカンダリーバッファのリストア

プライマリーバッファへの書き込みを許可モード(DSSCL_WRITEPRIMARY)にすると、アプリケーションは直接プライマリーバッファへ書き込むことができます。プライマリーバッファへの書き込みアクセスが実行されると、セカンダリーバッファのミキシング機能が停止します。そのため、他のすべてのアプリケーションのセカンダリーバッファは再生できなくなります。このときセカンダリーバッファは「失われた状態」として扱われ、DSBSTATUS_BUFFERLOSTフラグが立ちます。

セカンダリーバッファを再生する時は、DSBSTATUS_BUFFERLOSTフラグが立っていないかチェックしなければなりません。もしバッファが失われた状態になっているときは、復帰処理を行います。以下のコードは、復帰処理のサンプルです。

    DWORD dwStatus;
    HRESULT hr = m_pDSBuffer->GetStatus(&dwStatus);
    if (FAILED(hr))             return hr;

    if (dwStatus & DSBSTATUS_BUFFERLOST)
    {
        do
        {
            hr = m_pDSBuffer->Restore();
            if (hr == DSERR_BUFFERLOST)     Sleep(10);
        }
        while (hr != DS_OK);
    }

IDirectSoundBuffer8::GetStatus()関数でステータスを取得し、DSBSTATUS_BUFFERLOSTフラグをチェックします。もしフラグが立っていたら、IDirectSoundBuffer8::Restore()関数を実行します。ただしバッファの内部処理に時間がかかることがあるため、関数が成功するまで時間を置いて再試行します。

リストアが終了したら、バッファにサウンドデータを読み込みます。

ドキュメントの先頭へ

カクタスソフトウェア 技術協力 資料室 資料室の広場 SourceForge.jp お問い合わせ