Exclusive-Mode 流
如前所述,如果应用程序以独占模式打开流,应用程序将独占使用播放或录制流的音频终结点设备。 相比之下,多个应用程序可以通过在设备上打开共享模式流来共享音频终结点设备。
对音频设备的独占模式访问可能会阻止关键的系统声音,防止与其他应用程序的互作性,否则会降低用户体验。 为了缓解这些问题,具有独占模式流的应用程序通常会在应用程序不是前台进程或未主动流式传输时放弃对音频设备的控制。
流延迟是连接应用程序终结点缓冲区与音频终结点设备的数据路径固有的延迟。 对于呈现流,延迟是应用程序将示例写入终结点缓冲区到通过扬声器听到样本的时间之间的最大延迟。 对于捕获流,延迟是从声音进入麦克风到应用程序可以从终结点缓冲区读取该声音样本的时间的最大延迟。
使用独占模式流的应用程序通常这样做,因为它们需要在音频终结点设备和访问终结点缓冲区的应用程序线程之间的数据路径中低延迟。 通常,这些线程以相对较高的优先级运行,并计划自己定期运行,这些间隔接近或与分隔音频硬件连续处理传递的定期间隔相同。 每次传递期间,音频硬件都会处理终结点缓冲区中的新数据。
若要实现最小的流延迟,应用程序可能需要特殊的音频硬件和轻加载的计算机系统。 使音频硬件超出计时限制或加载具有竞争性高优先级任务的系统可能会导致低延迟音频流出现故障。 例如,对于呈现流,如果应用程序在音频硬件读取缓冲区之前无法写入终结点缓冲区,或者硬件在计划播放缓冲区之前未能读取缓冲区,则可能会出现故障。 通常,旨在在各种音频硬件上运行且在各种系统中运行的应用程序应放宽其计时要求,以避免所有目标环境中的故障。
Windows Vista 具有多个功能来支持需要低延迟音频流的应用程序。 如 User-Mode 音频组件中所述,执行时间关键作的应用程序可以调用多媒体类计划程序服务(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 提供 ISimpleAudioVolume、IChannelAudioVolume,IAudioStreamVolume 接口来控制共享模式音频流的音量级别。 但是,这些接口中的控件对独占模式流没有影响。 相反,管理独占模式流的应用程序通常使用 EndpointVolume API 中的 IAudioEndpointVolume 接口来控制这些流的卷级别。 有关此接口的信息,请参阅 终结点卷控制。
对于系统中的每个播放设备和捕获设备,用户可以控制设备是否可以在独占模式下使用。 如果用户禁用设备的独占模式使用,则设备可用于播放或仅录制共享模式流。
如果用户启用对设备的独占模式使用,则用户还可以控制应用程序在独占模式下使用设备的请求是否将占用当前可能通过设备播放或录制共享模式流的应用程序使用设备。 如果启用抢占,则应用程序对设备进行独占控制的请求成功(如果设备当前未使用),或者设备在共享模式下使用,但如果另一个应用程序已拥有设备的独占控制权,则请求会失败。 如果已禁用抢占,则应用程序对设备进行独占控制的请求成功(如果设备当前未使用),但如果设备已在共享模式或独占模式下使用,则请求将失败。
在 Windows Vista 中,音频终结点设备的默认设置如下:
- 设备可用于播放或录制独占模式流。
- 使用设备播放或录制独占模式流的请求会抢占当前通过设备播放或录制的任何共享模式流。
更改播放或录制设备 的独占模式设置
- 右键单击位于任务栏右侧的通知区域中的扬声器图标,然后选择 播放设备 或 录制设备。 (或者,从命令提示符窗口运行 Windows 多媒体控制面板,Mmsys.cpl。有关详细信息,请参阅 DEVICE_STATE_XXX 常量中的备注。)
- 显示 声音 窗口后,选择 播放 或 录制。 接下来,在设备名称列表中选择一个条目,然后单击 属性。
- 显示 属性 窗口后,单击 高级。
- 若要使应用程序能够以独占模式使用设备,请选中标记为 允许应用程序对此设备进行独占控制。 若要禁用设备的独占模式使用,请清除复选框。
- 如果启用了设备的独占模式使用,则可以指定当设备当前正在播放或录制共享模式流时,对设备的独占控制请求是否成功。 若要将独占模式应用程序优先于共享模式应用程序,请选中标记为 为独占模式应用程序优先级的框。 若要拒绝独占模式应用程序优先于共享模式应用程序,请清除复选框。
以下代码示例演示如何在配置为在独占模式下使用的音频呈现设备上播放低延迟音频流:
//-----------------------------------------------------------
// 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 函数调用将还原原始线程优先级。
有关 AvSetMmThreadCharacteristics 和 AvRevertMmThreadCharacteristics的详细信息,请参阅 Windows SDK 文档。
在流运行时,在前面的代码示例中 循环时,的每个迭代都会填充一个终结点缓冲区。 在迭代之间,WaitForSingleObject 函数调用等待事件句柄发出信号。 发出句柄信号时,循环正文执行以下作:
- 调用 IAudioRenderClient::GetBuffer 方法来获取下一个缓冲区。
- 填充缓冲区。
- 调用 IAudioRenderClient::ReleaseBuffer 方法来释放缓冲区。
有关 WaitForSingleObject的详细信息,请参阅 Windows SDK 文档。
如果音频适配器由 WaveRT 驱动程序控制,则事件句柄的信号将绑定到来自音频硬件的 DMA 传输通知。 对于 USB 音频设备,或者对于由 WaveCyclic 或 WavePci 驱动程序控制的音频设备,事件句柄的信号将绑定到从应用程序缓冲区传输到硬件缓冲区的 IRP 完成。
前面的代码示例将音频硬件和计算机系统推送到其性能限制。 首先,为了减少流延迟,应用程序会计划其缓冲区服务线程,以使用音频硬件将支持的最低设备周期。 其次,为了确保线程在每个设备周期内可靠地执行,AvSetMmThreadCharacteristics 函数调用会将 TaskName 参数设置为“Pro Audio”,即 Windows Vista 中具有最高优先级的默认任务名称。 考虑应用程序的时间要求是否可能放宽,而不会影响其有用性。 例如,应用程序可能会计划其缓冲区服务线程,以使用比最小值更长的时间段。 较长时间可能会安全地允许使用较低的线程优先级。
相关主题