排他モード ストリーム

前述のように、アプリケーションが排他モードでストリームを開いた場合、アプリケーションはストリームを再生または記録するオーディオ エンドポイント デバイスを排他的に使用します。 これに対し、デバイスで共有モード ストリームを開くことにより、複数のアプリケーションがオーディオ エンドポイント デバイスを共有できます。

オーディオ デバイスへの排他モード アクセスにより、重要なシステム サウンドがブロックされたり、他のアプリケーションとの相互運用性がなくなったり、ユーザー エクスペリエンスが低下したりする可能性があります。 これらの問題を軽減するため、排他モード ストリームを使用するアプリケーションは通常、アプリケーションがフォアグラウンド プロセスではないときやアクティブにストリーミングしていないとき、オーディオ デバイスの制御を放棄します。

ストリーム待ち時間は、アプリケーションのエンドポイント バッファーをオーディオ エンドポイント デバイスに接続するデータ パスに固有の遅延です。 レンダリング ストリームの場合、待ち時間は、アプリケーションがエンドポイント バッファーにサンプルを書き込む時間から、サンプルがスピーカーを介して聞こえるまでの最大遅延です。 キャプチャ ストリームの場合、待ち時間は、サウンドがマイクに入った時間から、アプリケーションがそのサウンドのサンプルをエンドポイント バッファーから読み取ることができる時間までの最大遅延です。

排他モード ストリームを使用するアプリケーションは多くの場合、オーディオ エンドポイント デバイスとエンドポイント バッファーにアクセスするアプリケーション スレッド間のデータ パスで待ち時間が短い必要があるため、これを行います。 通常、これらのスレッドは比較的高い優先度で実行され、連続する処理パスをオーディオ ハードウェアごとに分離する定期間隔に近いか同じ間隔で実行されるようスケジュールします。 各パスの間、オーディオ ハードウェアはエンドポイント バッファー内の新しいデータを処理します。

ストリームの最小待ち時間を実現するには、アプリケーションで特殊なオーディオ ハードウェアと、軽量で読み込まれるコンピューター システムの両方が必要になることがあります。 タイミング制限を超えてオーディオ ハードウェアを駆動したり、競合する優先度の高いタスクでシステムを読み込んだりすると、低遅延のオーディオ ストリームで障害が発生する可能性があります。 たとえば、レンダリング ストリームの場合、オーディオ ハードウェアがバッファーを読み取る前にアプリケーションがエンドポイント バッファーへの書き込みに失敗した場合や、バッファーの再生がスケジュールされる前にハードウェアがバッファーの読み取りに失敗した場合、障害が発生する可能性があります。 通常、さまざまなオーディオ ハードウェアおよび幅広いシステムで実行されることを目的としたアプリケーションでは、すべてのターゲット環境で障害が発生しないようタイミング要件を十分緩和する必要があります。

Windows Vista には、低遅延のオーディオ ストリームを必要とするアプリケーションをサポートするいくつかの機能があります。 ユーザー モード オーディオ コンポーネントで説明したように、タイム クリティカルな操作を実行するアプリケーションは、マルチ メディア クラス スケジューラー サービス (MMCSS) 関数を呼び出して、優先順位の低いアプリケーションに対して CPU リソースを拒否することなく、スレッドの優先度を高めることができます。 さらに、IAudioClient::Initialize メソッドは AUDCLNT_STREAMFLAGS_EVENTCALLBACK フラグをサポートしています。このフラグは、アプリケーションのバッファー サービス スレッドが、オーディオ デバイスから新しいバッファーが使用可能になったときに実行をスケジュールできるようにします。 これらの機能を使用することにより、アプリケーション スレッドは、実行されるタイミングに関する不確実性を下げ、低遅延のオーディオ ストリームで障害が発生するリスクを軽減できます。

古いオーディオ アダプターのドライバーは、多くの場合 WaveCyclic または WavePci デバイス ドライバー インターフェイス (DDI) を使用しますが、新しいオーディオ アダプターのドライバーは WaveRT DDI をサポートする可能性の方が高くなります。 排他モード アプリケーションの場合、WaveRT ドライバーは WaveCyclic ドライバーまたは WavePci ドライバーよりも優れたパフォーマンスを実現しますが、WaveRT ドライバーには追加のハードウェア機能が必要です。 これらの機能には、ハードウェア バッファーをアプリケーションと直接共有する機能が含まれます。 直接共有では、排他モード アプリケーションとオーディオ ハードウェアの間でデータが転送されるため、システムの介入は必要ありません。 これに対し、WaveCyclic ドライバーと WavePci ドライバーは、古くて能力の低いオーディオ アダプターに適しています。 これらのアダプターは、アプリケーション バッファーとハードウェア バッファーの間でデータ ブロック (システム I/O 要求パケットまたは IRP に接続) を転送する点でシステム ソフトウェアに依存しています。 さらに、USB オーディオ デバイスは、アプリケーション バッファーとハードウェア バッファーの間でデータを転送するシステム ソフトウェアに依存しています。 データ転送においてシステムに依存するオーディオ デバイスに接続する排他モード アプリケーションのパフォーマンスを向上させるため、WASAPI は、アプリケーションとハードウェアの間でデータを転送するシステム スレッドの優先順位を自動的に高めます。 WASAPI は、MMCSS を使用してスレッドの優先度を高めます。 Windows Vista では、PCM 形式でデバイス期間が 10 ミリ秒未満の排他的モード オーディオ再生ストリームのデータ転送をシステム スレッドが管理する場合、WASAPI は MMCSS タスク名 "Pro Audio" をスレッドに割り当てます。 ストリームのデバイス期間が 10 ミリ秒以上の場合、WASAPI は MMCSS タスク名 "Audio" をスレッドに割り当てます。 WaveCyclic、WavePci、WaveRT DDI について詳しくは、Windows DDK のドキュメントをご覧ください。 適切なデバイス期間の選択については、「IAudioClient::GetDevicePeriod」をご覧ください。

セッション ボリューム コントロール」で説明されているとおり、WASAPI は、共有モード オーディオ ストリームのボリューム レベルを制御するための ISimpleAudioVolumeIChannelAudioVolume、および IAudioStreamVolume インターフェイスを提供します。 ただし、これらのインターフェイスのコントロールは、排他モード ストリームには影響を与えません。 代わりに、排他モード ストリームを管理するアプリケーションでは通常、EndpointVolume APIIAudioEndpointVolume インターフェイスを使用して、これらのストリームのボリューム レベルを制御します。 このインターフェイスについて詳しくは、「エンドポイント ボリューム コントロール」をご覧ください。

システム内の再生デバイスとキャプチャ デバイスごとに、ユーザーはデバイスを排他モードで使用できるかどうかを制御できます。 ユーザーがデバイスの排他モードの使用を無効にした場合、デバイスを使用して共有モード ストリームのみを再生または記録できます。

ユーザーがデバイスの排他モードの使用を有効にした場合、排他モードでデバイスを使用するアプリケーションによる要求が、現在デバイスを介して共有モード ストリームを再生または記録している可能性があるアプリケーションによるデバイスの使用を優先するかどうかをユーザーが制御することもできます。 プリエンプションが有効になっていて、デバイスが現在使用されていない場合、またはデバイスが共有モードで使用されている場合、アプリケーションによるデバイスの排他制御の要求は成功しますが、別のアプリケーションが既にデバイスを排他的に制御している場合、要求は失敗します。 プリエンプションが無効になっていて、デバイスが現在使用されていない場合、アプリケーションによるデバイスの排他制御の要求は成功しますが、デバイスが既に共有モードまたは排他モードで使用されている場合、要求は失敗します。

Windows Vista では、オーディオ エンドポイント デバイスの既定の設定は次のとおりです。

  • デバイスを使用して、排他モード ストリームを再生または記録することができます。
  • デバイスを使用して排他モード ストリームの再生または記録を行う要求は、デバイスを介して現在再生または記録されている共有モード ストリームを優先します。

再生または記録デバイスの排他モード設定を変更するには

  1. タスク バーの右側にある通知領域のスピーカー アイコンを右クリックし、[再生デバイス] または [録音デバイス] を選択します。 (別の方法として、コマンド プロンプト ウィンドウから Windows マルチメディア コントロール パネル Mmsys.cpl を実行します。詳しくは、「DEVICE_STATE_XXX 定数」の「解説」をご覧ください)。
  2. [サウンド] ウィンドウが表示されたら、[再生] または [録音] を選択します。 次に、デバイス名の一覧でエントリを選択し、[プロパティ] をクリックします。
  3. [プロパティ] ウィンドウが表示されたら、[詳細設定] をクリックします。
  4. アプリケーションが排他モードでデバイスを使用可能にするには、[アプリケーションがこのデバイスを排他的に制御できるようにする] というラベルのボックスをオンにします。 デバイスの排他モードの使用を無効にするには、チェック ボックスをオフにします。
  5. デバイスの排他モードの使用が有効になっている場合は、デバイスが現在再生中の場合や共有モード ストリームを記録している場合にデバイスの排他制御の要求が成功するかどうかを指定できます。 排他モード アプリケーションを共有モード アプリケーションよりも優先させるには、[排他的モード アプリケーションの優先度を指定する] というラベルのボックスをオンにします。 共有モード アプリケーションよりも排他モードのアプリケーションの優先度を下げるには、チェック ボックスをオフにします。

次のコード例は、排他モードで使用するよう構成されたオーディオ レンダリング デバイスで、低遅延のオーディオ ストリームを再生する方法を示しています。

//-----------------------------------------------------------
// Play an exclusive-mode stream on the default audio
// rendering device. The PlayExclusiveStream function uses
// event-driven buffering and MMCSS to play the stream at
// the minimum latency supported by the device.
//-----------------------------------------------------------

// REFERENCE_TIME time units per second and per millisecond
#define REFTIMES_PER_SEC  10000000
#define REFTIMES_PER_MILLISEC  10000

#define EXIT_ON_ERROR(hres)  \
              if (FAILED(hres)) { goto Exit; }
#define SAFE_RELEASE(punk)  \
              if ((punk) != NULL)  \
                { (punk)->Release(); (punk) = NULL; }

const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
const IID IID_IAudioClient = __uuidof(IAudioClient);
const IID IID_IAudioRenderClient = __uuidof(IAudioRenderClient);

HRESULT PlayExclusiveStream(MyAudioSource *pMySource)
{
    HRESULT hr;
    REFERENCE_TIME hnsRequestedDuration = 0;
    IMMDeviceEnumerator *pEnumerator = NULL;
    IMMDevice *pDevice = NULL;
    IAudioClient *pAudioClient = NULL;
    IAudioRenderClient *pRenderClient = NULL;
    WAVEFORMATEX *pwfx = NULL;
    HANDLE hEvent = NULL;
    HANDLE hTask = NULL;
    UINT32 bufferFrameCount;
    BYTE *pData;
    DWORD flags = 0;
    DWORD taskIndex = 0;
    
    hr = CoCreateInstance(
           CLSID_MMDeviceEnumerator, NULL,
           CLSCTX_ALL, IID_IMMDeviceEnumerator,
           (void**)&pEnumerator);
    EXIT_ON_ERROR(hr)

    hr = pEnumerator->GetDefaultAudioEndpoint(
                        eRender, eConsole, &pDevice);
    EXIT_ON_ERROR(hr)

    hr = pDevice->Activate(
                    IID_IAudioClient, CLSCTX_ALL,
                    NULL, (void**)&pAudioClient);
    EXIT_ON_ERROR(hr)

    // Call a helper function to negotiate with the audio
    // device for an exclusive-mode stream format.
    hr = GetStreamFormat(pAudioClient, &pwfx);
    EXIT_ON_ERROR(hr)

    // Initialize the stream to play at the minimum latency.
    hr = pAudioClient->GetDevicePeriod(NULL, &hnsRequestedDuration);
    EXIT_ON_ERROR(hr)

    hr = pAudioClient->Initialize(
                         AUDCLNT_SHAREMODE_EXCLUSIVE,
                         AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
                         hnsRequestedDuration,
                         hnsRequestedDuration,
                         pwfx,
                         NULL);
    if (hr == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED) {
        // Align the buffer if needed, see IAudioClient::Initialize() documentation
        UINT32 nFrames = 0;
        hr = pAudioClient->GetBufferSize(&nFrames);
        EXIT_ON_ERROR(hr)
        hnsRequestedDuration = (REFERENCE_TIME)((double)REFTIMES_PER_SEC / pwfx->nSamplesPerSec * nFrames + 0.5);
        hr = pAudioClient->Initialize(
            AUDCLNT_SHAREMODE_EXCLUSIVE,
            AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
            hnsRequestedDuration,
            hnsRequestedDuration,
            pwfx,
            NULL);
    }
    EXIT_ON_ERROR(hr)

    // Tell the audio source which format to use.
    hr = pMySource->SetFormat(pwfx);
    EXIT_ON_ERROR(hr)

    // Create an event handle and register it for
    // buffer-event notifications.
    hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
    if (hEvent == NULL)
    {
        hr = E_FAIL;
        goto Exit;
    }

    hr = pAudioClient->SetEventHandle(hEvent);
    EXIT_ON_ERROR(hr);

    // Get the actual size of the two allocated buffers.
    hr = pAudioClient->GetBufferSize(&bufferFrameCount);
    EXIT_ON_ERROR(hr)

    hr = pAudioClient->GetService(
                         IID_IAudioRenderClient,
                         (void**)&pRenderClient);
    EXIT_ON_ERROR(hr)

    // To reduce latency, load the first buffer with data
    // from the audio source before starting the stream.
    hr = pRenderClient->GetBuffer(bufferFrameCount, &pData);
    EXIT_ON_ERROR(hr)

    hr = pMySource->LoadData(bufferFrameCount, pData, &flags);
    EXIT_ON_ERROR(hr)

    hr = pRenderClient->ReleaseBuffer(bufferFrameCount, flags);
    EXIT_ON_ERROR(hr)

    // Ask MMCSS to temporarily boost the thread priority
    // to reduce glitches while the low-latency stream plays.
    hTask = AvSetMmThreadCharacteristics(TEXT("Pro Audio"), &taskIndex);
    if (hTask == NULL)
    {
        hr = E_FAIL;
        EXIT_ON_ERROR(hr)
    }

    hr = pAudioClient->Start();  // Start playing.
    EXIT_ON_ERROR(hr)

    // Each loop fills one of the two buffers.
    while (flags != AUDCLNT_BUFFERFLAGS_SILENT)
    {
        // Wait for next buffer event to be signaled.
        DWORD retval = WaitForSingleObject(hEvent, 2000);
        if (retval != WAIT_OBJECT_0)
        {
            // Event handle timed out after a 2-second wait.
            pAudioClient->Stop();
            hr = ERROR_TIMEOUT;
            goto Exit;
        }

        // Grab the next empty buffer from the audio device.
        hr = pRenderClient->GetBuffer(bufferFrameCount, &pData);
        EXIT_ON_ERROR(hr)

        // Load the buffer with data from the audio source.
        hr = pMySource->LoadData(bufferFrameCount, pData, &flags);
        EXIT_ON_ERROR(hr)

        hr = pRenderClient->ReleaseBuffer(bufferFrameCount, flags);
        EXIT_ON_ERROR(hr)
    }

    // Wait for the last buffer to play before stopping.
    Sleep((DWORD)(hnsRequestedDuration/REFTIMES_PER_MILLISEC));

    hr = pAudioClient->Stop();  // Stop playing.
    EXIT_ON_ERROR(hr)

Exit:
    if (hEvent != NULL)
    {
        CloseHandle(hEvent);
    }
    if (hTask != NULL)
    {
        AvRevertMmThreadCharacteristics(hTask);
    }
    CoTaskMemFree(pwfx);
    SAFE_RELEASE(pEnumerator)
    SAFE_RELEASE(pDevice)
    SAFE_RELEASE(pAudioClient)
    SAFE_RELEASE(pRenderClient)

    return hr;
}

前のコード例では、PlayExclusiveStream 関数は、レンダリング ストリームの再生中にエンドポイント バッファーを処理するアプリケーション スレッドで実行されます。 この関数は、クライアント定義クラス MyAudioSource に属するオブジェクトへのポインターである 1 つのパラメーター pMySource を受け取ります。 このクラスには、コード例で呼び出される 2 つのメンバー関数 LoadData および SetFormat があります。 MyAudioSource については、「ストリームのレンダリング」をご覧ください。

PlayExclusiveStream 関数は、既定のレンダリング デバイスとネゴシエートするヘルパー関数 GetStreamFormat を呼び出し、アプリケーションで使用するのに適した排他モード ストリーム形式をデバイスがサポートしているかどうかを判断します。 GetStreamFormat 関数のコードは、コード例には示されていません。これは、実装の詳細がアプリケーションの要件に完全に依存するためです。 ただし、GetStreamFormat 関数の操作は簡単に記述することができます。IAudioClient::IsFormatSupported メソッドを 1 回以上呼び出して、デバイスが適切な形式をサポートしているかどうかを判断します。 アプリケーションの要件によって、GetStreamFormat が IsFormatSupported メソッドに提示する形式と、その形式が提示される順序が決まります。 IsFormatSupported について詳しくは、「デバイスの形式」をご覧ください。

GetStreamFormat 呼び出しの後、PlayExclusiveStream 関数は IAudioClient::GetDevicePeriod メソッドを呼び出し、オーディオ ハードウェアでサポートされている最小デバイス期間を取得します。 次に、この関数は IAudioClient::Initialize メソッドを呼び出して、最小期間と等しいバッファー期間を要求します。 呼び出しに成功した場合、Initialize メソッドは 2 つのエンドポイント バッファーを割り当てます。各バッファーは期間が最小期間に等しくなります。 その後、オーディオ ストリームの実行が開始されると、アプリケーションとオーディオ ハードウェアは "ピンポン" の手法で 2 つのバッファーを共有します。つまり、アプリケーションが 1 つのバッファーに書き込んでいる間、ハードウェアはもう一方のバッファーから読み取ります。

ストリームを開始する前に、PlayExclusiveStream 関数は次の処理を行います。

  • バッファーを埋める準備ができたときに通知を受け取るイベント ハンドルを作成して登録します。
  • ストリームの実行が開始されてから最初のサウンドが聞こえるまでの遅延を減らすため、オーディオ ソースのデータを最初のバッファーに入力します。
  • AvSetMmThreadCharacteristics 関数を呼び出し、PlayExclusiveStream を実行するスレッドの優先度を MMCSS が上げることを要求します。 (ストリームの実行が停止すると、AvRevertMmThreadCharacteristics 関数の呼び出しは、元のスレッドの優先度を復元します)。

AvSetMmThreadCharacteristicsAvRevertMmThreadCharacteristics について詳しくは、Windows SDK のドキュメントをご覧ください。

ストリームの実行中に、前のコード例の while ループの各イテレーションによって、1 つのエンドポイント バッファーがいっぱいになります。 繰り返しの間、WaitForSingleObject 関数呼び出しはイベント ハンドルが通知されるまで待機します。 ハンドルが通知されると、ループ本体は次の処理を行います。

  1. IAudioRenderClient::GetBuffer メソッドを呼び出して、次のバッファーを取得します。
  2. バッファーを埋めます。
  3. IAudioRenderClient::ReleaseBuffer メソッドを呼び出してバッファーを解放します。

WaitForSingleObject の詳細については、Windows SDK のマニュアルを参照してください。

オーディオ アダプターが WaveRT ドライバーによって制御されている場合、イベント ハンドルのシグナリングは、オーディオ ハードウェアからの DMA 転送通知に関連付けられます。 USB オーディオ デバイスの場合、または WaveCyclic ドライバーまたは WavePci ドライバーによって制御されるオーディオ デバイスの場合、イベント ハンドルのシグナリングは、アプリケーション バッファーからハードウェア バッファーにデータを転送する IRP の完了に関連付けられます。

前のコード例では、オーディオ ハードウェアとコンピューター システムをパフォーマンスの制限にプッシュします。 まず、ストリームの待ち時間を短縮するため、アプリケーションは、オーディオ ハードウェアがサポートする最小デバイス期間を使用するようバッファー サービス スレッドをスケジュールします。 次に、各デバイス期間内にスレッドが確実に実行されるよう、AvSetMmThreadCharacteristics 関数呼び出しは、TaskName パラメーターを "Pro Audio" に設定します。これは、Windows Vista では、優先度が最も高い既定のタスク名です。 アプリケーションのタイミング要件を、その有用性を損なうことなく緩和できるかどうかを検討します。 たとえば、アプリケーションでは、最小よりも長い期間を使用するようバッファー サービス スレッドをスケジュールできます。 期間がより長ければ、より低いスレッド優先度の使用を安全に許可できる可能性があります。

ストリームの管理