投稿日 : 2015/11/06 3:33:33
WASAPIをプログラムしてみる

WASAPIのプログラムの流れ

WASAPIはCOMを使ったライブラリです。ちなみにCOMとは簡単に言うとOSに組み込まれたライブラリで、C++のほかVBやC#などCOMに対応した言語から利用が可能ですが、ここではVisualC++を使用した場合の使用方法について説明しています。

なお、WASAPIには排他モードと共有モードがありますが、ここでは排他モードを利用した実装を行います。

WASAPIはだいたい以下の流れで実装します。
  1. COMの初期化
  2. サウンドデバイスの列挙
  3. 使用するデバイスの選択
  4. デバイスからオーディオクライアントの取得
  5. オーディオクライアントのフォーマットをセット
  6. オーディオクライアントを初期化
  7. イベントオブジェクトの構築
  8. オーディオクライアントにイベントオブジェクトを設定
  9. オーディオクライアントからレンダークライアントを取得
  10. レンダークライアントをリセット
  11. 再生スレッドの構築
  12. オーディオクライアントの開始
  13. <ここでスレッドにてサウンドデータをレンダークライアントに流し込んでいく>
  14. 再生スレッドを終了
  15. イベントオブジェクトを解放
  16. レンダークライアントを解放
  17. オーディオクライアントを解放
  18. デバイスを開放
  19. サウンド列挙子を開放
  20. COMの終了

ヘッダとライブラリ、インターフェース

WASAPIのAPIを使用するには以下のヘッダをインクルードしてください。
#include <mmdeviceapi.h>
#include <Audioclient.h>
#include <audiopolicy.h>
#include <endpointvolume.h>
#include <FunctionDiscoveryKeys_devpkey.h>
また、以下のライブラリとリンクする必要があります。
Avrt.lib
なお、プロジェクトの設定にて毎回定義するのが面倒な場合、VisualC++ではコード中に以下のように記述することで、ビルド時に自動的にリンクさせることが出来ます。
#pragma comment(lib, "Avrt.lib")
WASAPIの各インターフェースのCLSIDやIIDは、上記のヘッダファイルに参照のみが定義されており、実体はどのライブラリにも含まれていません。このため、ソースコード(例えばmain.cppなど)のどこかに以下のようなコードを記述しておく必要があります。
const CLSID CLSID_MMDeviceEnumerator    = __uuidof(MMDeviceEnumerator);
const IID IID_IMMDeviceEnumerator       = __uuidof(IMMDeviceEnumerator);
const IID IID_IAudioClient              = __uuidof(IAudioClient);
const IID IID_IAudioClock               = __uuidof(IAudioClock);
const IID IID_IAudioRenderClient        = __uuidof(IAudioRenderClient);

COMのお約束

WASAPIを使用するにはCOMの初期化が必要です。このため、WinMainの直後などに以下のコードを追加しておきます。COMを使ったプログラムには必ず登場するので覚えておいてください。
    CoInitialize( NULL );
なお、COMの使用を終了する場合は以下のコードを追加します。
    CoUninitialize();
使用中のCOMオブジェクトを開放する前にこのコードを呼び出してしまうと、アプリがハングアップしてしまうので、COMが確実に開放されていることを確認してから呼び出すようにしてください。

また、CoInitialize()とCoUninitialize()は必ず対で使用しなければなりません。このため、CoInitialize()を呼び出したままCoUninitialize()を忘れてアプリを終了してしまうと、終了時にデバッガーにエラーが表示されてしまうことがあります。なお、対で使用するのであればこれらを何回でも呼び出すことが可能です。

さらに重要なこととして、CoInitialize()とCoUninitialize()はスレッドごとに呼び出す必要があります。具体的にはCreateThread()などで別スレッドを生成し、そのスレッド内でCOMオブジェクトを構築する場合、WinMain内でCOMの初期化を行ったかどうかに関係無く、新たにそのスレッド内でCOMの初期化と終了を行わなければなりません。
※別スレッドで構築されたCOMオブジェクトにアクセスする分には初期化は必要ありません

WASAPIの初期化

ここではWASAPIを使用して単一のWAVをループ再生するプログラムのサンプルを作成してみます。

まずはWASAPIのインターフェースや、再生処理に必要となるイベントやスレッドなどをグローバル変数として定義しておきます。
IMMDeviceEnumerator     *pDeviceEnumerator  = NULL;     // マルチメディアデバイス列挙インターフェース
IMMDevice               *pDevice            = NULL;     // デバイスインターフェース
IAudioClient            *pAudioClient       = NULL;     // オーディオクライアントインターフェース
IAudioRenderClient      *pRenderClient      = NULL;     // レンダークライアントインターフェース
int                     iFrameSize          = 0;        // 1サンプル分のバッファサイズ

HANDLE                  hEvent              = NULL;     // イベントハンドル
HANDLE                  hThread             = NULL;     // スレッドハンドル
BOOL                    bThreadLoop         = FALSE;    // スレッド処理中か
WASAPIを使うにはPCに存在するサウンドデバイスを選択して、そのデバイスのオーディオクライアントインターフェースを取得します。以下はデフォルトのサウンドデバイスに対して構築する場合のコードとなります。
    HRESULT ret;

    // マルチメディアデバイス列挙子
    ret = CoCreateInstance( CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, IID_IMMDeviceEnumerator, (void**)&pDeviceEnumerator );
    if( FAILED(ret) ) {
        DEBUG( "CLSID_MMDeviceEnumeratorエラー¥n" );
        return FALSE;
    }

    // デフォルトのデバイスを選択
    ret = pDeviceEnumerator->GetDefaultAudioEndpoint( eRender,eConsole,&pDevice );
    if( FAILED(ret) ) {
        DEBUG( "GetDefaultAudioEndpointエラー¥n" );
        return FALSE;
    }

    // オーディオクライアント
    ret = pDevice->Activate( IID_IAudioClient, CLSCTX_ALL, NULL, (void**)&pAudioClient );
    if( FAILED(ret) ) {
        DEBUG( "オーディオクライアント取得失敗¥n" );
        return FALSE;
    }
次に、このデバイスを排他モードで使用する時のフォーマットを決めます。デバイスが対応しているフォーマットを設定しないと、初期化に失敗してしまうので注意してください。また、この時のサンプルサイズはあとで使用するため、一緒にグローバル変数に保存しておきます。
    // フォーマットの構築
    WAVEFORMATEXTENSIBLE wf;
    ZeroMemory( &wf,sizeof(wf) );
    wf.Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE);
    wf.Format.wFormatTag            = WAVE_FORMAT_EXTENSIBLE;
    wf.Format.nChannels             = 2;
    wf.Format.nSamplesPerSec        = 44100;
    wf.Format.wBitsPerSample        = 16;
    wf.Format.nBlockAlign           = wf.Format.nChannels * wf.Format.wBitsPerSample / 8;
    wf.Format.nAvgBytesPerSec       = wf.Format.nSamplesPerSec * wf.Format.nBlockAlign;
    wf.Samples.wValidBitsPerSample  = wf.Format.wBitsPerSample;
    wf.dwChannelMask                = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
    wf.SubFormat                    = KSDATAFORMAT_SUBTYPE_PCM;

    // 1サンプルのサイズを保存(16bitステレオなら4byte)
    iFrameSize = wf.Format.nBlockAlign;
そしてこのフォーマットが今回のサウンドデバイスに設定出来るかチェックします。ここでは排他モードとして、第1引数にAUDCLNT_SHAREMODE_EXCLUSIVEを指定します。
    // フォーマットのサポートチェック
    ret = pAudioClient->IsFormatSupported( AUDCLNT_SHAREMODE_EXCLUSIVE,(WAVEFORMATEX*)&wf,NULL );
    if( FAILED(ret) ) {
        DEBUG( "未サポートのフォーマット¥n" );
        return FALSE;
    }
最後にサウンドデバイスを初期化しますが、この時バッファを転送してから実際に再生されるまでの遅延時間(レイテンシ)を指定します。ただし、この値は必ずデバイスにあったものでなければならず、初期化に失敗した場合の戻り値がAUDCLNT_E_BUFFER_SIZE_NOT_ALIGNEDだった場合は、初期化関数が内部で修正後のバッファサイズを計算してくれるので、この値を取得して再度初期化を行います。
※遅延時間の値は100ナノ秒単位のため、ミリ秒に変換するには10000で割ります。逆にミリ秒からナノ秒にする場合は10000を掛けますが、ここで使用する値は64bit値となるため、64bitの値を表現する場合は数値の最後にLL(LongLongの略)を付けます。

遅延時間について
サウンド再生というのは、まずある程度まとまったサウンドバッファをデバイスに転送しておき、デバイスはこの受け取ったバッファを順番にアナログ変換することで行います。そして再生中のバッファが足りなくなると追加のバッファを要求するといった仕様になっており、このため次に書き込んだバッファが実際に再生されるまで、必ず遅延時間というものが存在します。また、デバイスにはそれぞれ再生速度やデータの転送速度などを考慮し、デバイスが最速で再生出来る遅延時間として最小デバイスピリオドという値が定義されています。ただし、実際にこの最小デバイスピリオドを設定出来たとしても、現状どんなに高速なPCだとしても処理が間に合わずにノイズが発生するため、あまり実用的ではありません。代わりに通常の再生に問題の無い遅延時間として、デフォルトデバイスピリオドという値が定義されています。一般的なサウンドカードではこの値は10msに設定されているので、通常はこの値を使って初期化すると良いでしょう。
    // レイテンシ設定
    REFERENCE_TIME default_device_period = 0;
    REFERENCE_TIME minimum_device_period = 0;

    if( latency!=0 ) {
        default_device_period = (REFERENCE_TIME)latency * 10000LL;      // デフォルトデバイスピリオドとしてセット
        DEBUG( "レイテンシ指定             : %I64d (%fミリ秒)¥n",default_device_period,default_device_period/10000.0 );
    } else {
        ret = pAudioClient->GetDevicePeriod( &default_device_period,&minimum_device_period );
        DEBUG( "デフォルトデバイスピリオド : %I64d (%fミリ秒)¥n",default_device_period,default_device_period/10000.0 );
        DEBUG( "最小デバイスピリオド       : %I64d (%fミリ秒)¥n",minimum_device_period,minimum_device_period/10000.0 );
    }

    // 初期化
    UINT32 frame = 0;
    ret = pAudioClient->Initialize( AUDCLNT_SHAREMODE_EXCLUSIVE,
                                    AUDCLNT_STREAMFLAGS_NOPERSIST | AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
                                    default_device_period,              // デフォルトデバイスピリオド値をセット
                                    default_device_period,              // デフォルトデバイスピリオド値をセット
                                    (WAVEFORMATEX*)&wf,
                                    NULL ); 
    if( FAILED(ret) ) {
        if( ret==AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED ) {
            DEBUG( "バッファサイズアライメントエラーのため修正する¥n" );

            // 修正後のフレーム数を取得
            ret = pAudioClient->GetBufferSize( &frame );
            DEBUG( "修正後のフレーム数         : %d¥n",frame );
            default_device_period = (REFERENCE_TIME)( 10000.0 *                     // (REFERENCE_TIME(100ns) / ms) *
                                                      1000 *                        // (ms / s) *
                                                      frame /                       // frames /
                                                      wf.Format.nSamplesPerSec +    // (frames / s)
                                                      0.5);                         // 四捨五入?
            DEBUG( "修正後のレイテンシ         : %I64d (%fミリ秒)¥n",default_device_period,default_device_period/10000.0  );

            // 一度破棄してオーディオクライアントを再生成
            SAFE_RELEASE( pAudioClient );
            ret = pDevice->Activate( IID_IAudioClient, CLSCTX_ALL, NULL, (void**)&pAudioClient );
            if( FAILED(ret) ) {
                DEBUG( "オーディオクライアント再取得失敗¥n" );
                return FALSE;
            }

            // 再挑戦
            ret = pAudioClient->Initialize( AUDCLNT_SHAREMODE_EXCLUSIVE,
                                            AUDCLNT_STREAMFLAGS_NOPERSIST | AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
                                            default_device_period,
                                            default_device_period,
                                            (WAVEFORMATEX*)&wf,
                                            NULL );
        }

        if( FAILED(ret) ) {
            DEBUG( "初期化失敗 : 0x%08X¥n",ret );
            return FALSE;
        }
    }
初期化が済んだらイベントオブジェクトを作成して、オーディオクライアントに登録します。これにより、バッファが足りなくなるとイベントがシグナル状態となるため、このタイミングで次のバッファを転送させることが出来るようになります(のちほど説明)。
    // イベント生成
    hEvent = CreateEvent( NULL,FALSE,FALSE,NULL );
    if( !hEvent ) {
        DEBUG( "イベントオブジェクト作成失敗¥n" );
        return FALSE;
    }

    // イベントのセット
    ret = pAudioClient->SetEventHandle( hEvent );
    if( FAILED(ret) ) {
        DEBUG( "イベントオブジェクト設定失敗¥n" );
        return FALSE;
    }
次にオーディオクライアントからサウンドバッファを書き込むためのレンダラーを取得します。
    // レンダラーの取得
    ret = pAudioClient->GetService( IID_IAudioRenderClient,(void**)&pRenderClient );
    if( FAILED(ret) ) {
        DEBUG( "レンダラー取得エラー¥n" );
        return FALSE;
    }
そしてここが重要ですが、本来レンダラーが構築されたらそのバッファはクリアされているはずですが、USBDACなどドライバによっては初期化が行わておらず、ゴミが残った状態でバッファ要求イベントが発生し、結果的にノイズが発生するといった問題があります。そのため、最初にレンダラー内のバッファを強制的にクリアしておきます。なお、この時のバッファサイズはオーディオクライアントから取得しますが、取得出来るのはフレーム数となるため、実際のバッファサイズとするには1フレーム分のサイズ(16bitステレオの場合は4byte)にこのフレーム数を掛けて求めます。
    // WASAPI情報取得
    ret = pAudioClient->GetBufferSize( &frame );
    DEBUG( "設定されたフレーム数       : %d¥n",frame );

    UINT32 size = frame * iFrameSize;
    DEBUG( "設定されたバッファサイズ   : %dbyte¥n",size );
    DEBUG( "1サンプルの時間            : %f秒¥n",(float)size/wf.Format.nSamplesPerSec );

    // ゼロクリアをしてイベントをリセット
    LPBYTE pData;
    ret = pRenderClient->GetBuffer( frame,&pData );
    if( SUCCEEDED(ret) ) {
        ZeroMemory( pData,size );
        pRenderClient->ReleaseBuffer( frame,0 );
    }
次にこのバッファ要求イベントを受け取り、要求があればサウンドバッファを生成して転送するためのスレッドを作成します。
※スレッドループフラグはスレッドを終了させるためのフラグで、詳細はスレッド内のコードを参照してください
    // スレッドループフラグを立てる
    bThreadLoop = TRUE;

    // 再生スレッド起動
    DWORD dwThread;
    hThread = CreateThread( NULL,0,PlayThread,NULL,0,&dwThread );
    if( !hThread ) {
        // 失敗
        return FALSE;
    }
最後にWASAPIのサウンド処理を開始させます。
    // 再生開始
    pAudioClient->Start();
これ以降スレッドにバッファ要求が行われ、それに合わせてサウンドバッファの転送処理を行うことで実際に音が再生されるようになります。

サウンドスレッド

サウンドスレッドでは、サウンドデバイスからのバッファ要求イベントを待ち、イベントが発生したら次のサウンドデータを転送し、再び要求待ちを行うといったループ処理を行います。イベントを待つ関数はWaitForSingleObject()を使用しますが、この関数は引数で指定した時間になるまで処理を停止するので、イベントが発生しない限りCPU使用率がほぼ0となります。

以下はスレッド処理のサンプルです。
DWORD CALLBACK PlayThread( LPVOID param )
{
    DEBUG( "スレッド開始¥n" );

    while( bThreadLoop ) {
        // 次のバッファ取得が必要になるまで待機
        DWORD retval = WaitForSingleObject( hEvent,2000 );
        if( retval!=WAIT_OBJECT_0 ) {
            DEBUG( "タイムアウト¥n" );
            pAudioClient->Stop();
            break;
        }

        // 今回必要なフレーム数を取得
        UINT32 frame_count;
        HRESULT ret = pAudioClient->GetBufferSize( &frame_count );
//      ODS( "フレーム数    : %d¥n",frame_count );

        // フレーム数からバッファサイズを算出
        int buf_size = frame_count * iFrameSize;
//      ODS( "バッファサイズ : %dbyte¥n",buf_size );


        // 出力バッファのポインタを取得
        LPBYTE dst;
        ret = pRenderClient->GetBuffer( frame_count,&dst );
        if( SUCCEEDED(ret) ) {
            // ■成功ならここで出力バッファに指定サイズ分のデータを書き込む

            // バッファを開放
            pRenderClient->ReleaseBuffer( frame_count,0 );
        }
    }

    DEBUG( "スレッド終了¥n" );
    return 0;
}
要求イベントが発生したらまず書き込むバッファのポインタを取得します。そしてそこに次のサウンドデータを書き込みます。最後に書き込みが終了したらそのバッファを開放することで、デバイスにデータが転送されたことになり、再生がそのまま継続されます。

この流れを繰り返すことでサウンドを途切れさせずに鳴らすことが出来ますが、これはあくまでもデータ転送の方が再生より速い場合の理想な状態です。

例えばもしサウンドデータを計算で生成する場合、処理が間に合わなくなるとデバイス上のデータが足りなくなってしまうため、その間一時的に音が途切れることになります。また、この転送が間に合わない状態が連続で発生した場合は、結果的にノイズとして聞こえてしまう原因になります。さらに、仮にWAVデータを順番に転送するような設計だとして、この時コピーが間に合わず何度も音が途切れた状態になると、その間サウンドデータが進まないことになるため、結果的に音がどんどん遅れていくといった現象が発生します。

ちなみにデフォルトデバイスピリオド値をレイテンシに設定した場合で、WAVデータを単に順番に転送するだけなら基本的に間に合わないということはありませんが、ゲームで使用するとなるとたくさんのサウンドをミキシングしなければならないため、ミキシング数が多くなると処理が間に合わず、これがノイズとなってしまうことがあります。

これらを踏まえ、WASAPIを使用するということは処理が間に合う前提で設計しなければならず、PCのスペックが足りないなどで処理が間に合わないのであれば、レイテンシを多めに取ってみたり、そもそもWASAPIをやめてDirectSoundに切り替えるといった方法も考えておく必要があります。
※そのため本家で公開中のcharatbeatHDXやBMIIDXView2015では、これらを任意に切り替えられるようになっています

WASAPIの終了処理

WASAPIを終了させるにはまずスレッドを終了させ、その後WASAPIインターフェースを開放します。

スレッドが残ったまま先にインターフェースを終了してしまうと、スレッド上では以前のインターフェースに対してアクセスしてしまうことになるため、アクセスバイオレーションによりアプリがクラッシュしてしまいます。これを避けるために確実にスレッドが終了するまでインターフェースを開放しないといった制御を行う必要があります。

以下はそれを踏まえたコードとなります。
    // スレッドループフラグを落とす
    bThreadLoop = FALSE;

    // スレッド終了処理
    if( hThread ) {
        // スレッドが完全に終了するまで待機
        WaitForSingleObject( hThread,INFINITE );
        CloseHandle( hThread );
        hThread = NULL;
    }

    // イベントを開放処理
    if( hEvent ) {
        CloseHandle( hEvent );
        hEvent = NULL;
    }

    // インターフェース開放
    SAFE_RELEASE( pRenderClient );
    SAFE_RELEASE( pAudioClient );
    SAFE_RELEASE( pDevice );
    SAFE_RELEASE( pDeviceEnumerator );
まずスレッドループフラグをFALSEにすることで、スレッド中のループから抜けられるようにします。これにより次のバッファ要求イベントが発生した際、ループから抜けてスレッドが終了出来るようになります。

なお、マルチスレッドプログラミングではメイン側でスレッドループフラグをFALSEに設定したとしても、スレッド側がそのループフラグを見るまでは実際に終了しないので、そのスレッドがいつ終わるのかは未知です。例えばSleep()などで1秒くらい待てば終了しているだろうといった考え方もありますが、もしCPUが他の処理に取られていれば当然1秒以上かかることもあるので、時間で終わったかどうかを判断するのは危険です。

Windowsにはスレッドが確実に終わったかを判断する手法が用意されており、ここではその方法を使用します。具体的にはスレッドを作成した際のスレッドハンドルをWaitForSingleObject()に渡すことで、スレッドが終了するまでこの関数でブロックされます。これによりスレッドが終わったことが確実に保障されるので、そのあとでスレッドハンドルやイベントオブジェクト、WASAPIインターフェースを全て開放します。

ちなみにインターフェースの開放にSAFE_RELEASEというマクロを使用していますが、これは以下のように定義しておきます。
※COMプログラムでよく使われます
#define SAFE_RELEASE(x)     { if( x ) { x->Release(); x=NULL; } }
これで使用していたCOMオブジェクトがきれいに開放されたことになるので、アプリの終了前に最後にCoUninitialize()を呼び出してCOMを終了させます。

サンプルプログラム

上記の内容を踏まえ、単一のWAVファイルをループ再生するサンプルを用意しました。
TestWasapi.zipのダウンロード
※これはVisualC++2010のプロジェクトとなります
コメントはまだ登録されていません。
コメントする
名前
コメント
※タグは使用出来ません
この記事に関連するタグ
WASAPI