低延迟音频

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

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

本文档的目标是:

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

本文介绍:

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

术语

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

Windows 音频堆栈

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

Diagram showing the low latency audio stack with apps, audio engine driver, and hardware.

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

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

  2. 音频引擎从缓冲区读取数据并处理此类数据。 它还以音频处理对象 (APO) 的形式加载音频效果。 有关 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. 音频引擎从缓冲区读取数据并处理此类数据。 它还以音频处理对象 (APO) 的形式加载音频效果。

  7. APO 的延迟会因 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 返回引擎为指定流格式支持的周期范围
InitializeSharedAudioStream 使用指定的周期初始化共享流

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 的应用程序也使用实时工作队列 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,而不是 OS 根据编解码器链接位置猜测。 这有助于 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 驱动程序,而不是第三方编解码器驱动程序。 请记住之前使用的驱动程序,以便要对音频编解码器使用最佳设置时,可以回退到该驱动程序。

Graph illustrating roundtrip latency differences between WASAPI and AudioGraph for various buffer sizes.

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 毫秒缓冲区。