低延迟音频

本文讨论 Windows 10 中的音频延迟更改。 它介绍应用程序开发人员的 API 选项,以及可用于支持低延迟音频的驱动程序的更改。 音频延迟是创建声音的时间和听到声音的时间之间的延迟。 对于以下几个关键方案,低音频延迟非常重要,例如:

  • 专业音频
  • 音乐创作
  • 通信
  • 虚拟现实
  • 游戏

本文档的目标是:

  1. 介绍 Windows 中的音频延迟源。
  2. 说明在 Windows 10 音频堆栈中减少音频延迟的更改。
  3. 提供有关应用程序开发人员和硬件制造商如何利用新基础结构的参考,以便开发低音频延迟的应用程序和驱动程序。

本文介绍:

  1. 用于交互式和媒体创建方案的 AudioGraph API。
  2. WASAPI 中的更改以支持低延迟。
  3. 驱动程序 DDI 中的增强功能。

术语

Term 描述
呈现延迟 应用程序将音频数据缓冲区提交到呈现 API 的时间直到听到来自扬声器的声音的时间之间的延迟。
捕获延迟 从麦克风捕获声音到将其传输到应用程序所用捕获 API 之间的延迟。
往返延迟 麦克风中捕获声音的时间、由应用程序处理和由应用程序提交以便呈现给扬声器的时间之间的延迟。 它大致等于呈现延迟 + 捕获延迟。
触碰到应用延迟 在用户点击屏幕到将信号发送到应用程序的时间之间延迟。
触碰到声音延迟 用户点击屏幕的时间、事件转到应用程序并通过扬声器听到声音的时间之间的延迟。 它等于呈现延迟 + 触碰到应用延迟。

Windows 音频堆栈

下图显示了 Windows 音频堆栈的简化版本。

图显示了应用、音频引擎驱动程序和硬件的低延迟音频堆栈。

下面是呈现路径中延迟的摘要:音频处理对象

  1. 应用程序将数据写入缓冲区

  2. 音频引擎从缓冲区读取数据并对其进行处理。 它还以音频处理对象(ADO)的形式加载音频效果。 有关 APO 的详细信息,请参阅 Windows 音频处理对象

  3. APO 的延迟因 APO 中的信号处理而异。

  4. 在 Windows 10 之前,对于使用浮点数据的应用程序,音频引擎的延迟等于约 12 毫秒,对于使用整数数据的应用程序,延迟为约 6 毫秒

  5. 在 Windows 10 及更高版本中,所有应用程序的延迟已减少到 1.3 毫秒

  6. 音频引擎将处理的数据写入缓冲区。

  7. 在 Windows 10 之前,缓冲区始终设置为约 10 毫秒。

  8. 从 Windows 10 开始,缓冲区大小由音频驱动程序定义(本文稍后将介绍缓冲区的更多详细信息)。

  9. 音频驱动程序从缓冲区读取数据并将其写入硬件。

  10. 硬件还可以以更多音频效果的形式再次处理数据。

  11. 用户听到来自扬声器的音频。

下面是捕获路径中延迟的摘要:

  1. 音频是从麦克风捕获的。

  2. 硬件可以处理数据。 例如,添加音频效果。

  3. 驱动程序从硬件读取数据并将数据写入缓冲区。

  4. 在 Windows 10 之前,此缓冲区始终设置为 10 毫秒。

  5. 从 Windows 10 开始,缓冲区大小由音频驱动程序定义(下面更多详细信息)。

  6. 音频引擎从缓冲区读取数据并处理它们。 它还以音频处理对象(ADO)的形式加载音频效果。

  7. APO 的延迟因其内部的信号处理而异。

  8. 在 Windows 10 之前,对于使用浮点数据的应用程序,音频引擎的延迟等于约 6 毫秒,对于使用整数数据的应用程序,延迟为约 0 毫秒。

  9. 在 Windows 10 及更高版本中,所有应用程序的延迟已减少到约 0 毫秒。

  10. 应用程序表示,一旦音频引擎完成处理,就可以读取数据。 音频堆栈还提供独占模式的选项。 在这种情况下,数据会绕过音频引擎,直接从应用程序转到驱动程序从中读取它的缓冲区。 但是,如果应用程序以独占模式打开终结点,则没有其他应用程序可以使用该终结点来呈现或捕获音频。

需要低延迟的应用程序的另一种常用替代方法是使用 ASIO(音频流输入/输出)模型,该模型利用独占模式。 用户安装第三方 ASIO 驱动程序后,应用程序可以直接将数据从应用程序发送到 ASIO 驱动程序。 但是,应用程序必须以与 ASIO 驱动程序直接对话的方式编写。

这两种替代项(独占模式和 ASIO)都有自己的限制。 它们提供低延迟,但它们具有自己的限制(其中一些限制如上所述)。 因此,音频引擎已修改,以降低延迟,同时保持灵活性。

音频堆栈改进

Windows 10 及更高版本在三个方面进行了增强,以减少延迟:

  1. 与 Windows 8.1 相比,所有使用音频的应用程序在不需要更改代码或更新驱动程序的情况下,其往返延迟将减少 4.5-16 毫秒(如上述部分所述)。
    1. 使用浮点数据的应用程序的延迟将降低 16 毫秒。
    2. 使用整数数据的应用程序的延迟将降低 4.5 毫秒。
  2. 具有更新驱动程序的系统将提供更低的往返延迟:
    1. 驱动程序可以使用低延迟 DDI 来报告用于在 Windows 和硬件之间传输数据的缓冲区所支持的大小。 数据传输不必始终使用 10 毫秒的缓冲区,就像在以前的 Windows 版本中一样。 相反,驱动程序可以指定它是否可以使用小型缓冲区,例如 5 毫秒、3 毫秒、1 毫秒等。
    2. 需要低延迟的应用程序可以使用低延迟音频 API(AudioGraph 或 WASAPI)来查询驱动程序支持的缓冲区大小,并选择一个用于与硬件之间传输数据的缓冲区。
  3. 当应用程序使用低于特定阈值的缓冲区大小来呈现和捕获音频时,Windows 将进入特殊模式,在该模式下,它以避免音频流式处理和其他子系统之间的干扰的方式管理其资源。 这将减少音频子系统的执行中断,并最大限度地减少音频故障的概率。 当应用程序停止流式处理时,Windows 将返回到其正常执行模式。 音频子系统包含以下资源:
    1. 正在处理低延迟音频的音频引擎线程。
    2. 驱动程序注册的所有线程和中断(使用有关驱动程序资源注册的部分中所述的低延迟 DDI)。
    3. 来自请求小缓冲区的应用程序的部分或全部音频线程,以及所有与请求小缓冲区的应用程序共享相同音频设备图(例如,相同信号处理模式)的应用程序:
  4. 流式处理路径上的 AudioGraph 回调。
  5. 如果应用程序使用 WASAPI,则仅提交到 实时工作队列 APIMFCreateMFByteStreamOnStreamEx 并标记为“Audio”或“ProAudio”的工作项。

API 改进

以下两个 Windows 10 API 提供低延迟功能:

若要确定要使用的两个 API 中的哪一个:

  • 尽可能支持 AudioGraph,以便进行新的应用程序开发。
  • 仅在以下的情况下使用 WASAPI:
    • 你需要比 AudioGraph 提供的更多的控制权。
    • 需要比 AudioGraph 提供的延迟低的延迟。

本文的测量工具部分显示了使用收件箱 HDAudio 驱动程序从 Haswell 系统进行的特定测量。

以下部分将介绍每个 API 中的低延迟功能。 如上一部分所述,为了使系统达到最低延迟,它需要更新的驱动程序支持较小的缓冲区大小。

AudioGraph

AudioGraph 是 Windows 10 及更高版本中的通用 Windows 平台 API,旨在轻松实现交互式和音乐创作方案。 AudioGraph 以多种编程语言(C++、C#、JavaScript)提供,具有简单且功能丰富的编程模型。

为了针对低延迟场景,AudioGraph 提供 AudioGraphSettings::QuantumSizeSelectionMode 属性。 此属性可以是下表中显示的任何值:

描述
SystemDefault 将缓冲区设置为默认缓冲区大小(约 10 毫秒)
LowestLatency 将缓冲区设置为驱动程序支持的最小值
ClosestToDesired 将缓冲区大小设置为等于 DesiredSamplesPerQuantum 属性定义的值或与驱动程序支持的与 DesiredSamplesPerQuantum 一样接近的值。

AudioCreation 示例 演示如何将 AudioGraph 用于低延迟。 以下代码片段演示如何设置最小缓冲区大小:

AudioGraphSettings settings = new AudioGraphSettings(AudioRenderCategory.Media);
settings.QuantumSizeSelectionMode = QuantumSizeSelectionMode.LowestLatency;
CreateAudioGraphResult result = await AudioGraph.CreateAsync(settings);

Windows 音频会话 API (WASAPI)

从 Windows 10 开始,WASAPI 已增强为:

  • 允许应用程序发现给定音频设备的音频驱动程序支持的缓冲区大小范围(即周期值)。 这使得应用程序可以在在共享模式下打开流时在默认缓冲区大小(10 毫秒)或小缓冲区(小于 10 毫秒)之间进行选择。 如果应用程序未指定缓冲区大小,则它将使用默认缓冲区大小。
  • 允许应用程序发现音频引擎的当前格式和周期性。 这样,应用程序就可以与音频引擎的当前设置保持一致。
  • 允许应用指定它希望以指定的格式呈现/捕获,而无需音频引擎进行任何重新采样

上述功能在所有 Windows 设备上都可用。 但是,具有足够资源和更新驱动程序的某些设备将提供比其他人更好的用户体验。

上述功能由一个名为 IAudioClient3的接口提供,该接口派生自 IAudioClient2

IAudioClient3 定义以下 3 种方法:

方法 描述
GetCurrentSharedModeEnginePeriod 返回音频引擎的当前格式和周期性
GetSharedModeEnginePeriod 返回引擎为指定流格式支持的周期范围
初始化共享音频流 使用指定的周期初始化共享流

WASAPIAudio 示例 演示了如何使用 IAudioClient3 实现低延迟。

以下代码片段演示了音乐创建应用如何在系统支持的最低延迟设置中运行。

// 1. Activation

// Get a string representing the Default Audio (Render|Capture) Device
m_DeviceIdString = MediaDevice::GetDefaultAudio(Render|Capture)Id(
Windows::Media::Devices::AudioDeviceRole::Default );

// This call must be made on the main UI thread.  Async operation will call back to
// IActivateAudioInterfaceCompletionHandler::ActivateCompleted, which must be an agile // interface implementation
hr = ActivateAudioInterfaceAsync( m_DeviceIdString->Data(), __uuidof(IAudioClient3),
nullptr, this, &asyncOp );

// 2. Setting the audio client properties – note that low latency offload is not supported

AudioClientProperties audioProps = {0};
audioProps.cbSize = sizeof( AudioClientProperties );
audioProps.eCategory = AudioCategory_Media;

// if the device has System.Devices.AudioDevice.RawProcessingSupported set to true and you want to use raw mode
// audioProps.Options |= AUDCLNT_STREAMOPTIONS_RAW;
//
// if it is important to avoid resampling in the audio engine, set this flag
// audioProps.Options |= AUDCLNT_STREAMOPTIONS_MATCH_FORMAT;


hr = m_AudioClient->SetClientProperties( &audioProps ); if (FAILED(hr)) { ... }

// 3. Querying the legal periods

hr = m_AudioClient->GetMixFormat( &mixFormat ); if (FAILED(hr)) { ... }

hr = m_AudioClient->GetSharedModeEnginePeriod(wfx, &defaultPeriodInFrames, &fundamentalPeriodInFrames, &minPeriodInFrames, &maxPeriodInFrames); if (FAILED(hr)) { ... }

// legal periods are any multiple of fundamentalPeriodInFrames between
// minPeriodInFrames and maxPeriodInFrames, inclusive
// the Windows shared-mode engine uses defaultPeriodInFrames unless an audio client // has specifically requested otherwise

// 4. Initializing a low-latency client

hr = m_AudioClient->InitializeSharedAudioStream(
         AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
         desiredPeriodInFrames,
         mixFormat,
         nullptr); // audio session GUID
         if (AUDCLNT_E_ENGINE_PERIODICITY_LOCKED == hr) {
         /* engine is already running at a different period; call m_AudioClient->GetSharedModeEnginePeriod to see what it is */
         } else if (FAILED(hr)) {
             ...
         }

// 5. Initializing a client with a specific format (if the format needs to be different than the default format)

AudioClientProperties audioProps = {0};
audioProps.cbSize = sizeof( AudioClientProperties );
audioProps.eCategory = AudioCategory_Media;
audioProps.Options |= AUDCLNT_STREAMOPTIONS_MATCH_FORMAT;

hr = m_AudioClient->SetClientProperties( &audioProps );
if (FAILED(hr)) { ... }

hr = m_AudioClient->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, appFormat, &closest);
if (S_OK == hr) {
       /* device supports the app format */
} else if (S_FALSE == hr) {
       /* device DOES NOT support the app format; closest supported format is in the "closest" output variable */
} else {
       /* device DOES NOT support the app format, and Windows could not find a close supported format */
}

hr = m_AudioClient->InitializeSharedAudioStream(
       AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
       defaultPeriodInFrames,
       appFormat,
       nullptr); // audio session GUID
if (AUDCLNT_E_ENGINE_FORMAT_LOCKED == hr) {
       /* engine is already running at a different format */
} else if (FAILED(hr)) {
       ...
}

此外,Microsoft 建议使用 WASAPI 的应用程序应使用 Real-Time 工作队列 APIMFCreateMFByteStreamOnStreamEx 来创建工作项,并将其标记为音频或专业音频,而不是使用它们自己的线程。 这将允许 Windows 以避免干扰非音频子系统的方式管理它们。 相比之下,所有 AudioGraph 线程都由 Windows 自动管理。 WASAPIAudio 示例中的以下代码片段演示如何使用 MF 工作队列 API。

// Specify Source Reader Attributes
Attributes->SetUnknown( MF_SOURCE_READER_ASYNC_CALLBACK, static_cast<IMFSourceReaderCallback *>(this) );
    if (FAILED( hr ))
    {
        goto exit;
    }
    Attributes->SetString( MF_READWRITE_MMCSS_CLASS_AUDIO, L"Audio" );
    if (FAILED( hr ))
    {
        goto exit;
    }
    Attributes->SetUINT32( MF_READWRITE_MMCSS_PRIORITY_AUDIO, 0 );
    if (FAILED( hr ))
    {
        goto exit;
    }
    // Create a stream from IRandomAccessStream
    hr = MFCreateMFByteStreamOnStreamEx (reinterpret_cast<IUnknown*>(m_ContentStream), &ByteStream );
    if ( FAILED( hr ) )
    {
        goto exit;
    }
    // Create source reader
    hr = MFCreateSourceReaderFromByteStream( ByteStream, Attributes, &m_MFSourceReader );

或者,以下代码片段演示如何使用 RT 工作队列 API。

#define INVALID_WORK_QUEUE_ID 0xffffffff
DWORD g_WorkQueueId = INVALID_WORK_QUEUE_ID;
//#define MMCSS_AUDIO_CLASS    L"Audio"
//#define MMCSS_PROAUDIO_CLASS L"ProAudio"

STDMETHODIMP TestClass::GetParameters(DWORD* pdwFlags, DWORD* pdwQueue)
{
       HRESULT hr = S_OK;
       *pdwFlags = 0;
       *pdwQueue = g_WorkQueueId;
       return hr;
}

//-------------------------------------------------------
STDMETHODIMP TestClass::Invoke(IRtwqAsyncResult* pAsyncResult)
{
       HRESULT hr = S_OK;
       IUnknown *pState = NULL;
       WCHAR className[20];
       DWORD  bufferLength = 20;
       DWORD taskID = 0;
       LONG priority = 0;

       printf("Callback is invoked pAsyncResult(0x%0x)  Current process id :0x%0x Current thread id :0x%0x\n", (INT64)pAsyncResult, GetCurrentProcessId(), GetCurrentThreadId());

       hr = RtwqGetWorkQueueMMCSSClass(g_WorkQueueId, className, &bufferLength);
       IF_FAIL_EXIT(hr, Exit);

       if (className[0])
       {
              hr = RtwqGetWorkQueueMMCSSTaskId(g_WorkQueueId, &taskID);
              IF_FAIL_EXIT(hr, Exit);

              hr = RtwqGetWorkQueueMMCSSPriority(g_WorkQueueId, &priority);
              IF_FAIL_EXIT(hr, Exit);
              printf("MMCSS: [%ws] taskID (%d) priority(%d)\n", className, taskID, priority);
       }
       else
       {
              printf("non-MMCSS\n");
       }
       hr = pAsyncResult->GetState(&pState);
       IF_FAIL_EXIT(hr, Exit);

Exit:
       return S_OK;
}
//-------------------------------------------------------

int _tmain(int argc, _TCHAR* argv[])
{
       HRESULT hr = S_OK;
       HANDLE signalEvent;
       LONG Priority = 1;
       IRtwqAsyncResult *pAsyncResult = NULL;
       RTWQWORKITEM_KEY workItemKey = NULL;;
       IRtwqAsyncCallback *callback = NULL;
       IUnknown *appObject = NULL;
       IUnknown *appState = NULL;
       DWORD taskId = 0;
       TestClass cbClass;
       NTSTATUS status;

       hr = RtwqStartup();
       IF_FAIL_EXIT(hr, Exit);

       signalEvent = CreateEvent(NULL, true, FALSE, NULL);
       IF_TRUE_ACTION_EXIT(signalEvent == NULL, hr = E_OUTOFMEMORY, Exit);

       g_WorkQueueId = RTWQ_MULTITHREADED_WORKQUEUE;

       hr = RtwqLockSharedWorkQueue(L"Audio", 0, &taskId, &g_WorkQueueId);
       IF_FAIL_EXIT(hr, Exit);

       hr = RtwqCreateAsyncResult(NULL, reinterpret_cast<IRtwqAsyncCallback*>(&cbClass), NULL, &pAsyncResult);
       IF_FAIL_EXIT(hr, Exit);

       hr = RtwqPutWaitingWorkItem(signalEvent, Priority, pAsyncResult, &workItemKey);
       IF_FAIL_EXIT(hr, Exit);

       for (int i = 0; i < 5; i++)
       {
              SetEvent(signalEvent);
              Sleep(30);
              hr = RtwqPutWaitingWorkItem(signalEvent, Priority, pAsyncResult, &workItemKey);
              IF_FAIL_EXIT(hr, Exit);
    }

Exit:
       if (pAsyncResult)
       {
              pAsyncResult->Release();
       }

      if (INVALID_WORK_QUEUE_ID != g_WorkQueueId)
      {
        hr = RtwqUnlockWorkQueue(g_WorkQueueId);
        if (FAILED(hr))
        {
            printf("Failed with RtwqUnlockWorkQueue 0x%x\n", hr);
        }

        hr = RtwqShutdown();
        if (FAILED(hr))
        {
            printf("Failed with RtwqShutdown 0x%x\n", hr);
        }
      }

       if (FAILED(hr))
       {
          printf("Failed with error code 0x%x\n", hr);
       }
       return 0;
}

最后,使用 WASAPI 的应用程序开发人员需要使用音频类别标记其流,以及是否基于每个流的功能使用原始信号处理模式。 Microsoft建议所有音频流不使用原始信号处理模式,除非理解影响。 原始模式绕过 OEM 选择的所有信号处理,因此:

  • 特定终结点的呈现信号可能欠佳。
  • 捕获信号可能采用应用程序无法理解的格式。
  • 延迟可能会得到改善。

驱动程序改进

为了使音频驱动程序支持低延迟,Windows 10 及更高版本提供以下功能:

  1. [必需]声明每个模式下支持的最小缓冲区大小。
  2. [可选,但建议]改进驱动程序和 Windows 之间的数据流协调。
  3. [可选,但建议]注册驱动程序资源(中断、线程),以便在低延迟方案中受 Windows 保护。 收件箱 HDAudio 总线驱动程序 hdaudbus.sys 枚举的 HDAudio 微型端口函数驱动程序无需注册 HDAudio 中断,因为 hdaudbus.sys 已完成此操作。 但是,如果微型端口驱动程序创建自己的线程,则需要注册它们。

以下三个部分将更深入地介绍每个功能。

声明最小缓冲区大小

驱动程序在 Windows、驱动程序和硬件之间移动音频数据时,在各种约束下运行。 这些约束可能是由于在内存和硬件之间移动数据的物理硬件传输,或者由于硬件或关联的 DSP 中的信号处理模块所致。

从 Windows 10 版本 1607 开始,驱动程序可以使用 DEVPKEY_KsAudio_PacketSize_Constraints2 设备属性来表达其缓冲区大小功能。 此属性允许用户定义驱动程序支持的绝对最小缓冲区大小,以及每个信号处理模式的特定缓冲区大小约束。 特定于模式的约束需要高于驱动程序的最小缓冲区大小,否则音频堆栈会忽略它们。

例如,以下代码片段演示驱动程序如何声明绝对支持的缓冲区大小为 2 毫秒,但默认模式支持 128 帧,如果假设采样率为 48-kHz,则对应于 3 毫秒。

 
//
// Describe buffer size constraints for WaveRT buffers
//
static struct
{
    KSAUDIO_PACKETSIZE_CONSTRAINTS2 TransportPacketConstraints;
    KSAUDIO_PACKETSIZE_PROCESSINGMODE_CONSTRAINT AdditionalProcessingConstraints[1];
} SysvadWaveRtPacketSizeConstraintsRender =
{
    {
        2 * HNSTIME_PER_MILLISECOND,                // 2 ms minimum processing interval
        FILE_BYTE_ALIGNMENT,                        // 1 byte packet size alignment
        0,                                          // no maximum packet size constraint
        2,                                          // 2 processing constraints follow
        {
            STATIC_AUDIO_SIGNALPROCESSINGMODE_DEFAULT,          // constraint for default processing mode
            128,                                                // 128 samples per processing frame
            0,                                                  // NA hns per processing frame
        },
    },
    {
        {
            STATIC_AUDIO_SIGNALPROCESSINGMODE_MOVIE,            // constraint for movie processing mode
            1024,                                               // 1024 samples per processing frame
            0,                                                  // NA hns per processing frame
        },
    }
};

有关这些结构的详细信息,请参阅以下文章:

此外,sysvad 示例 演示如何使用这些属性,以便驱动程序为每个模式声明最小缓冲区。

改进驱动程序和 OS 之间的协调

本部分中介绍的 DDI 允许驱动程序:

  • 明确指示缓冲区中可供 Windows 使用的一半(数据段),而不是让操作系统根据编解码器链接位置进行猜测。 这有助于 Windows 更快地从音频故障中恢复。
  • (可选)优化或简化其传入和传出 WaveRT 缓冲区的数据传输。 此处的好处量取决于 DMA 引擎设计或其他 WaveRT 缓冲区与 (可能为 DSP) 硬件之间的数据传输机制。
  • 如果驱动程序在内部累积捕获的数据,“突发”捕获的数据的速度比实时快。 这主要用于语音激活场景,但在正常流媒体播放期间也可以应用。
  • 提供有关其当前流位置的时间戳信息,而不是依靠 Windows 的猜测,这样可以可能提供更准确的位置信息。

这种 DDI 在使用 DSP 的情况下非常有用。 但是,标准 HD 音频驱动程序或其他简单的循环 DMA 缓冲区设计在此处列出的这些 DDI 中可能找不到很多好处。

多个驱动程序例程返回 Windows 性能计数器时间戳,反映设备捕获或显示样本的时间。

在具有复杂 DSP 管道和信号处理的设备中,计算准确的时间戳可能很有挑战性,应该深思熟虑地完成。 时间戳不应反映将样本传输到 Windows 或从 Windows 传输到 DSP 的时间。

若要计算性能计数器值,驱动程序和 DSP 可能采用以下一些方法。

  • 在 DSP 中,使用一些内部 DSP 时钟跟踪样本时间戳。
  • 在驱动程序与 DSP 之间,计算 Windows 性能计数器与 DSP 时钟之间的关联。 此过程可能从简单(但不太精确)到相当复杂或新颖(但更精确)。
  • 除非考虑这些延迟,否则考虑因信号处理算法或者管道或硬件传输而产生的任何持续延迟。

sysvad 示例演示如何使用上述 DDI。

注册驱动程序资源

为了帮助确保无故障运行,音频驱动程序必须在 Portcls 中注册其流式处理资源。 这允许 Windows 管理资源,以避免音频流式处理和其他子系统之间的干扰。

流资源是音频驱动程序用于处理音频流或确保音频数据流的任何资源。 仅支持两种类型的流资源:中断和驱动程序拥有的线程。 音频驱动程序应在创建资源后注册资源,并在删除资源之前注销该资源。

音频驱动程序可以在加载时初始化注册资源,也可以在运行时注册,比如在 I/O 资源重新分配时。 Portcls 使用全局状态来跟踪所有音频流资源。

在某些用例(例如需要非常低延迟的音频)的情况下,Windows 会尝试将音频驱动程序的注册资源与来自其他 OS、应用程序和硬件活动的干扰隔离开来。 OS 和音频子系统根据需要执行此操作,而无需与音频驱动程序交互,但音频驱动程序注册资源除外。

注册流资源的要求意味着流式处理管道路径中的所有驱动程序都必须直接或间接地向 Portcls 注册其资源。 音频微型端口驱动程序具有以下选项:

  • 音频微型端口驱动程序是其堆栈的底部驱动程序(直接连接 h/w),在这种情况下,驱动程序知道其流资源,并且可以将其注册到 Portcls。
  • 音频微型端口驱动程序在其他驱动程序(例如音频总线驱动程序)的帮助下传输音频。 这些其他驱动程序还使用必须在 Portcls 中注册的资源。 这些并行/总线驱动程序堆栈可以公开音频微型端口驱动程序用于收集此信息的公共或专用接口(如果单个供应商拥有所有驱动程序)。
  • 音频微型端口驱动程序正在在其他驱动程序(例如 hdaudbus)的帮助下传输音频。 这些其他驱动程序还使用必须在 Portcls 中注册的资源。 这些并行/总线驱动程序可以与 Portcls 链接并直接注册其资源。 音频微型端口驱动程序必须让 Portcls 知道它们依赖于这些其他并行/总线设备(PDO)的资源。 HD 音频基础结构使用此选项,即 HD 音频总线驱动程序与 Portcls 链接,并自动执行以下步骤:
    • 注册其总线驱动程序的资源,以及
    • 通知 Portcls 子级资源依赖于父级资源。 在 HD 音频体系结构中,音频微型端口驱动程序只需注册自己的驱动程序拥有的线程资源。

备注:

  • 收件箱 HDAudio 总线驱动程序 hdaudbus.sys 枚举的 HDAudio 微型端口函数驱动程序无需注册 HDAudio 中断,因为 hdaudbus.sys 已完成此操作。 但是,如果微型端口驱动程序创建自己的线程,则需要注册它们。
  • 与仅用于注册流式处理资源的 Portcls 链接的驱动程序必须更新其 INF,才能包括 wdmaudio.inf 并复制portcls.sys(和相关文件)。 wdmaudio.inf 中定义了一个新的 INF 复制节,以仅复制这些文件。
  • 仅在 Windows 10 及更高版本中运行的音频驱动程序可以硬链接到:
  • 必须在下层 OS 上运行的音频驱动程序可以使用以下接口(微型端口可以调用 IID_IPortClsStreamResourceManager 接口的 QueryInterface,并且仅在 PortCls 支持接口时注册其资源)。
  • 这些 DDI 使用此枚举和结构:

最后,出于注册资源的唯一目的链接到 PortCls 的驱动程序必须在其 inf 的 DDInstall 部分中添加以下两行。 音频微型端口驱动程序不需要此功能,因为它们已在 wdmaudio.inf 中包含/需要。

[<install-section-name>]
Include=wdmaudio.inf
Needs=WDMPORTCLS.CopyFilesOnly

上述行确保已安装 PortCls 及其相关文件。

度量工具

为了测量往返延迟,用户可以利用通过扬声器播放脉冲的工具,并通过麦克风捕获它们。 它们测量以下路径的延迟:

  1. 应用程序调用呈现 API(AudioGraph 或 WASAPI)来播放脉冲
  2. 音频通过扬声器播放
  3. 音频是从麦克风捕获的
  4. 捕获 API(AudioGraph 或 WASAPI)检测到脉冲,为了测量不同缓冲区大小的往返延迟,用户需要安装支持小型缓冲区的驱动程序。 收件箱 HDAudio 驱动程序已更新,以支持 128 个样本(2.66ms@48kHz)和 480 个样本(10ms@48kHz) 之间的缓冲区大小。 以下步骤演示如何安装收件箱 HDAudio 驱动程序(这是所有 Windows 10 和更高版本的 SKU 的一部分):
  • 启动设备管理器。
  • 在“声音视频和游戏控制器”下,双击对应于内部扬声器的设备。
  • 在下一个窗口中,转到 驱动程序 选项卡。
  • 选择“更新驱动程序”-“浏览我的计算机以获取驱动程序软件”-“让我从此计算机的设备驱动程序列表中选择”-“选择高清音频设备”,然后选择“下一步”。>>>
  • 如果出现标题为“更新驱动程序警告”的窗口,请选择“是”。
  • 选择 关闭
  • 如果要求重启系统,请选择“是”以重启。
  • 重新启动后,系统将使用收件箱Microsoft HDAudio 驱动程序,而不是第三方编解码器驱动程序。 请记住之前使用的驱动程序,以便如果要对音频编解码器使用最佳设置,则可以回退到该驱动程序。

图形,说明不同缓冲区大小的 WASAPI 和 AudioGraph 之间的往返延迟差异。

WASAPI 和 AudioGraph 之间的延迟差异是由于以下原因造成的:

  • AudioGraph 在捕获端添加了一个延迟缓冲区,以便同步呈现和捕获,WASAPI 不提供该缓冲区。 此添加简化了使用 AudioGraph 编写的应用程序的代码。
  • 当系统使用大于 6 毫秒的缓冲区时,AudioGraph 的呈现端还有另一个延迟缓冲区。
  • AudioGraph 不能选择禁用捕获音频效果。

样品

常见问题

如果所有应用程序都使用新 API 进行低延迟,那难道不是更好吗? 低延迟并不总是保证更好的用户体验?

不一定。 低延迟有其取舍:

  • 低延迟意味着更高的能耗。 如果系统使用 10 毫秒缓冲区,则表示 CPU 每 10 毫秒唤醒一次,填充数据缓冲区并进入睡眠状态。 但是,如果系统使用 1 毫秒缓冲区,则表示 CPU 每 1 毫秒唤醒一次。 第二种情况意味着 CPU 将更频繁地唤醒,并且消耗量将增加。 这将减少电池使用时间。
  • 大多数应用程序依赖于音频效果来提供最佳用户体验。 例如,媒体播放器希望提供高保真音频。 通信应用程序希望最小回声和噪音。 将这些类型的音频效果添加到流会增加其延迟。 这些应用程序对音频质量比音频延迟更感兴趣。

总之,每个应用程序类型对音频延迟有不同的需求。 如果应用程序不需要低延迟,则它不应将新 API 用于低延迟。

更新到 Windows 10 及更高版本的所有系统是否都会自动更新以支持小型缓冲区? 所有系统是否都支持相同的最小缓冲区大小?

不,为了使系统支持小缓冲区,需要更新驱动程序。 由 OEM 决定将更新哪些系统以支持小型缓冲区。 此外,较新的系统更有可能支持比旧系统更小的缓冲区。 新系统中的延迟很可能低于旧系统。

如果驱动程序支持较小的缓冲区大小,Windows 10 及更高版本中的所有应用程序是否都会自动使用小型缓冲区来呈现和捕获音频?

否,默认情况下,Windows 10 及更高版本中的所有应用程序都将使用 10 毫秒的缓冲区来呈现和捕获音频。 如果应用程序需要使用小型缓冲区,则需要使用新的 AudioGraph 设置或 WASAPI IAudioClient3 接口才能执行此操作。 但是,如果一个应用程序请求使用小缓冲区,则音频引擎将开始使用该特定缓冲区大小传输音频。 在这种情况下,使用同一终结点和模式的所有应用程序将自动切换到该小缓冲区大小。 当低延迟应用程序退出时,音频引擎将再次切换到 10 毫秒的缓冲区。