独占模式流

如前所述,如果应用程序以独占模式打开流,则该应用程序可独占使用播放或录制流的音频终结点设备。 相比之下,多个应用程序可以通过在设备上打开共享模式流来共享一个音频终结点设备。

以独占模式访问音频设备可能会阻止重要的系统声音,妨碍与其他应用程序的互操作性,并在其他方面降低用户体验。 为了减少这些问题,当应用程序并非前台进程或不主动进行流式传输时,具有独占模式流的应用程序通常会放弃对音频设备的控制。

流延迟是连接应用程序终结点缓冲器和音频终结点设备的数据路径中固有的延迟。 对于呈现流而言,延迟是指从应用程序将采样写入终结点缓冲区到扬声器听到该采样的最大延迟时间。 对于捕获流而言,延迟是从声音进入麦克风到应用程序从终结点缓冲区读取该声音样本的最大延迟时间。

使用独占模式流的应用程序通常要求音频终结点设备与访问终结点缓冲区的应用程序线程之间的数据路径延迟时间较短。 通常情况下,这些线程会以相对较高的优先级运行,并安排自己在周期性间隔内运行,该间隔接近或等于音频硬件连续处理通道之间的周期性间隔。 在每次传输过程中,音频硬件都会处理终结点缓冲区中的新数据。

为了实现最小的流延迟,应用程序可能需要特殊的音频硬件和负荷较小的计算机系统。 使音频硬件超出其计时限制或让系统加载相互竞争的高优先级任务,都可能会导致低延迟音频流出现故障。 例如,对于呈现流,如果应用程序在音频硬件读取缓冲区之前未能写入终结点缓冲区,或者硬件在缓冲区预定播放时间之前未能读取缓冲区,那么就会出现故障。 通常情况下,要在各种音频硬件和系统中运行的应用程序,应适当放宽时序要求,以避免在所有目标环境中出现故障。

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 提供了ISimpleAudioVolumeIChannelAudioVolumeIAudioStreamVolume 接口,用于控制共享模式音频流的音量级别。 但是,这些接口中的控制对独占模式流没有影响。 相反,管理独占模式流的应用程序通常使用 EndpointVolume API 中的 IAudioEndpointVolume 接口来控制这些流的音量级别。 有关此接口的信息,请参阅终结点音量控制

对于系统中的每个播放设备和捕获设备,用户都可以控制该设备是否可以在独占模式下使用。 如果用户禁用设备的独占模式使用,则设备只能用于播放或录制共享模式流。

如果用户启用了设备的独享模式使用,还可以控制应用程序以独享模式使用设备的请求是否会被当前可能正在通过设备播放或录制共享模式流的应用程序抢先使用。 在启用了抢占功能的情况下,如果设备当前未被使用,或如果设备正在共享模式下使用,则应用程序请求独占设备控制权会成功,但如果另一个应用程序已独占设备控制权,则请求会失败。 如果禁用了抢占功能,当应用程序请求独占设备控制权时,如果设备当前未被使用,则请求将成功;如果设备已在共享模式或独占模式下被使用,则请求会失败。

在 Windows Vista 中,音频终结点设备的默认设置如下:

  • 设备可用于播放或录制独占模式流。
  • 使用设备播放或录制独占模式流的请求会抢先播放或录制当前正在通过设备播放或录制的共享模式流。

更改播放或录制设备的独占模式设置

  1. 右键单击任务栏右侧通知区域中的扬声器图标,然后选择“播放设备”或“录制设备”。 (另一种方法是在命令提示符窗口中运行 Windows 多媒体控制面板 Mmsys.cpl。有关详细信息,请参阅 DEVICE_STATE_XXX Constants 中的备注。)
  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 函数会在为终结点缓冲区提供服务的应用程序线程中运行。 该函数采用单个参数 pMySource,它是一个指向属于客户定义类 MyAudioSource 的对象的指针。 该类有两个成员函数,即 LoadData 和 SetFormat,在代码示例中会调用这两个函数。 MyAudioSource 在呈现流中进行了介绍。

PlayExclusiveStream 函数会调用一个帮助程序函数 GetStreamFormat,该函数会与默认呈现设备协商,以确定该设备是否支持适合应用程序使用的独占模式流格式。 GetStreamFormat 函数的代码没有出现在代码示例中;这是因为实现该函数的详细信息完全取决于应用程序的要求。 但是,可以简单地描述 GetStreamFormat 函数的操作 — 它会调用一次或多次 IAudioClient::IsFormatSupported 方法,以确定设备是否支持合适的格式。 应用程序的要求决定了 GetStreamFormat 向 IsFormatSupported 方法提供哪些格式以及提供格式的顺序。 有关 IsFormatSupported 的更多信息,请参阅设备格式

在调用 GetStreamFormat 之后,PlayExclusiveStream 函数会调用 IAudioClient::GetDevicePeriod 方法,以获取音频硬件支持的最小设备周期。 接下来,函数会调用 IAudioClient::Initialize 方法,以请求让缓冲区持续时间等于最短周期。 如果调用成功,Initialize 方法会分配两个终结点缓冲区,每个缓冲区的持续时间等于最短周期。 之后,当音频流开始运行时,应用程序和音频硬件将以“ping-pong”方式共享两个缓冲区,即当应用程序向一个缓冲区写入时,硬件会从另一个缓冲区读取。

在启动流之前,PlayExclusiveStream 函数会执行以下操作:

  • 创建并注册事件句柄,当缓冲区准备好填充时,它将通过该事件句柄来接收通知。
  • 用音频源的数据填充第一个缓冲区,以减少从音频流开始运行到听到初始声音之间的延迟。
  • 调用 AvSetMmThreadCharacteristics 函数,以请求 MMCSS 提高执行 PlayExclusiveStream 的线程的优先级。 (当流停止运行时,AvRevertMmThreadCharacteristics 函数调用会还原原来的线程优先级。)

有关 AvSetMmThreadCharacteristicsAvRevertMmThreadCharacteristics 的详细信息,请参阅 Windows SDK 文档。

当流运行时,前面代码示例中 while 循环的每次迭代都会填充一个终结点缓冲区。 在两次迭代之间,WaitForSingleObject 函数调用会等待事件句柄发出信号。 发出句柄信号时,循环主体会执行以下操作:

  1. 调用 IAudioRenderClient::GetBuffer 方法获取下一个缓冲区。
  2. 填充缓冲区。
  3. 调用 IAudioRenderClient::ReleaseBuffer 方法以释放缓冲区。

有关 WaitForSingleObject 的详细信息,请参阅 Windows SDK 文档。

如果音频适配器由 WaveRT 驱动程序控制,则事件句柄的信号将与音频硬件的 DMA 传输通知绑定。 对于 USB 音频设备或由 WaveCyclic 或 WavePci 驱动程序控制的音频设备,事件句柄的信号与从应用程序缓冲区向硬件缓冲区传输数据的 IRP 的完成情况相关联。

前面的代码示例将音频硬件和计算机系统的性能推向了极限。 首先,为了减少流延迟,应用程序会安排其缓冲服务线程,以便使用音频硬件支持的最短设备周期。 其次,为确保线程在每个设备周期内可靠执行,AvSetMmThreadCharacteristics 函数调用会将 TaskName 参数设置为“Pro Audio”,在 Windows Vista 中,这是具有最高优先级的默认任务名。 考虑是否可以放宽应用程序的时间要求,避免影响其实用性。 例如,应用程序可能会安排其缓冲服务线程使用比最小值更长的周期。 如果时间较长,则可以安全地使用较低的线程优先级。

流管理