在音视频通信处理流程中,音频方面最基本的无外乎就是音频的采集和播放。windows 平台下,有很多音频采集播放的方法。作为一个 windows 端音频应用程序开发人员,经常会被各种可用的API淹没,比如 MME、DirectSound、WDM/KS 和 Core Audio。但是几乎所有做音视频通信的开发者都会选择 Core Audio 作为采集播放的底层 API。在本篇内容中我们将主要围绕 Core Audio,讲解它的优劣势,以及我们基于它来做 windows 音频采集播放的技术实践。
#1
Why Core Audio?
为什么选择 Core Audio?我们先来了解一下现在主流的一些 Windows APIs的优劣势。
1.1 Windows Multimedia Extensions (MME/WinMM)
MME是第一个适用于Windows的标准API。
优势:MME方法实现简单。
劣势:延时是一个重大的问题,动态,实时的音频(比如实时音频通话,游戏事件通知等)有点难以及时处理,一般最小时延能达到120ms。在实时音频场景中,任何比大脑认为应该发生的时间晚10毫秒的事情都被认为是不同步的。
1.2 DirectSound(DirectX Audio)
DirectX 是基于COM的多媒体API集合的总称,其中包括 DirectSound。
优势:
1)它可以非常接近硬件工作,极限最小延迟可到60毫秒左右,并支持更高质量的音频;
2)可通过简单的API使得与硬件交互变得切实可行;
3)为平台带来了可插拔的、基于软件的音频效果(DX 效果)和乐器(DXi Instruments)。
1.3 Windows Driver Model/Kernel 电脑 Streaming (WDM/KS)
使用 WDM 后,MME 和 DirectSound 音频现在都通过称为内核音频混合器(通常称为 KMixer)的东西。KMixer 是一个内核模式组件,负责将所有系统音频混合在一起。但是 KMixer 也会引入了延迟,大概30毫秒,事实上,有时会更多。为了减少 KMixer 带来的时延,WDM/KS 的方案诞生了。
优势:可将延迟做到极低的状态,一般最小延迟可以到1毫秒~10毫秒,且在一定情况下可以使用非分页内存、直接硬件IRP和RT,独占声卡的所有资源。
劣势:
1)独占了声卡的所有资源,导致只能听到特定应用程序的声音。当多个程序开启时,是无法听到其他应用程序的声音的;
2)KS 也没有音频输入,即麦克风也是无法使用的。
注意:在 Vista 和 Windows7之后,KMixer 已经被弃用了,KS并不适用于 Vista 和 Windows7之后的版本。
1.4 Audio Stream Input Output(ASIO)
ASIO 最初是 Windows 的专业音频级驱动程序规范,由一家名叫 Steinberg 的德国公司所提出的。
优势:为应用程序提供直接从应用程序到声音硬件的高质量、低延迟的数据路径。对于可以支持 ASIO 的应用程序,可以完全避免所有处理Windows 音频堆栈的业务,将系统对音频流的响应时间降至最短。使用 ASIO 的情况下,缓冲器依照设定的不同可至10毫秒以下,也有因环境较佳而到1毫秒以下的情况产生。
劣势:如果您尝试使用的音频应用程序仅支持 ASIO,而您的声卡是廉价的、缺乏 ASIO 支持的,那么使用 ASIO 就是一个问题了。ASIO 的实际性能取决于制造商提供的驱动程序的质量。
1.5 Windows Core Audio
2007年,Vista 电脑 最终上架时,Windows Core Audio 也面世了。微软宣称,vista/7 已经开始弃用了 kmixer 和依赖 dma 的 audio IO。熟悉和喜爱的所有音频API都被洗牌,突然发现自己建立在这个新的用户模式API之上。这包括 DirectSound,此时它完全失去了对硬件加速音频的支持。
优势:
1)低延迟、故障恢复的音频流;
2)提高可靠性(许多音频功能已从内核模式转移到用户模式);
3)提高安全性(受保护的音频内容的处理在安全、低权限的过程中进行);
4)将特定的系统范围角色(控制台、多媒体和通信)分配给各个音频设备;
5)用户直接操作的音频端点设备(例如,扬声器、耳机和麦克风)的软件抽象。
Windows 采集播放中有着多种API,但是大多数API都是位于 Core Audio之上,在实时音频领域应该推崇使用更接近底层的 API(ASIO或者Core Audio),可减少一定的时延。由于 ASIO 存在一定的局限性,Core Audio 更具有适用性。因此在现有大多数的 Windows 音视频通信客户端中采集播放使用的都是 Core Audio APIs。
#2
Core Audio 详解
Windows Core Audio,不要与 OSX 的类似名称的 Core Audio 混淆,它是对 Windows 上音频处理方式的彻底重新设计。大多数的音频组件从内核态转移到用户态,这对应用程序的稳定性产生了巨大的影响。几乎所有的Windows APIs 都建立在 Core Audio 之上。
2.1 Core Audio 系统内核框架详解
Core Audio 问世之后,新的音频系统内核框架也随之改变。
图一 基于Core Audio音频系统框架图
从系统框架图可以看到Core Audio APIs包含了4个API——MMDevice、WASAPI、DeviceTopology 和 EndpointVolume。
MMDevice API
客户端发现音频终端设备,枚举出所有可使用的音频设备属性以及确定其功能,并为这些设备创建驱动程序实例。是最基本的 Core Audio API,服务于其他3个 APIs。
WASAPI
客户端应用程序可以管理应用程序和音频终端设备之间音频数据的流。
DeviceTopology API
客户端可以遍历音频适配器设备和音频终端设备的内部拓扑,并单步执行将设备链接到另一台设备的连接。通过 DeviceTopology API 中的接口和方法,客户端程序可直接沿着音频适配器 (audio adapters) 的硬件设备里的数据通道进入布局特征(例如,沿着音频终端设备的数据路径上进行音量控制) 。
EndpointVolume API
客户端可以控制和监视音频终端设备的音量级别。
图中显示的是渲染的音频数据如何从大多数应用程序流向扬声器的简化表示。对于采集来说,音频数据的路径是完全相同,但流向是相反的。从图中可以看到,一些高阶API(例如MME,DirectSound等),对 Core Audio APIs 进行了封装,使用这些API能够更容易完成某些应用程序需求。但是对于音视频来说,需要减少时延使用更底层API。
从API处理后,音频流会经过两种路径到达音频端点缓存区——Shared Mode(共享模式)和Exclusive Mode(独占模式)。
共享模式和独占模式是 Core Audio 带来的一项重大改进。
共享模式
共享模式与旧的 KMixer 模式有一些相似之处。在共享模式下,应用程序写入传递给系统音频引擎的缓冲区。音频引擎负责将所有应用程序的音频混合在一起并将混合发送到音频驱动程序。与 KMixer 一样,这会引入延迟。音频引擎有时不仅需要转换音频数据,而且还必须混合来自多个共享模式应用程序的数据。这需要时间,通常是几毫秒。在大多数情况下,这种延迟是无法察觉的。
独占模式
独享是微软对专业音频世界的回应。独占模式的应用程序具有对硬件的独占访问权限,音频数据直接从应用程序传输到驱动程序再到硬件。独占模式的流媒体完全绕过了 Windows 音频引擎。它有效地锁定了所有的应用程序,相比于共享模式,独占模式音频的一个明显优势是,随着音频引擎的退出,它所带来的延迟被完全消除了。
但是独占模式流媒体的最大缺点是,对音频格式没有多少灵活性。只能使用音频适配器原生支持的格式。如果需要进行数据格式转换,应用程序将需要手动完成。值得指出的是,独占模式的流媒体实际上并不保证对应用程序可用。它是用户可配置的。用户可以为一个给定的音频适配器完全禁用独占模式音频。如下图:
图二 音频设备属性图
系统框架图中音频流最终流向了音频适配器。音频适配器很少有单一的输入和/或输出连接。事实上,大多数现代消费类个人电脑的音频适配器都支持至少三种类型的连接:耳机、扬声器和话筒。
在这一章,已经看到了音频流这个短语,音频流指的是应用程序和音频终端设备之间的一个连接。
2.2 Core Audio 的设备管理
2.2.1 设备的枚举
在音视频客户端设备列表中,客户通常能看到电脑上可使用的麦克风以及扬声器的列表。上面已经介绍过设备的枚举由 MMDevice API 控制,可以通过 MMDevice API 去枚举出设备数量及设备属性。首先需要通过 COM 接口来获取音频设备枚举实例,再通过 IMMDeviceEnumerator 对象去获取需要的设备属性。
constCLSIDCLSID_MMDeviceEnumerator=__uuidof(MMDeviceEnumerator);
constIIDIID_IMMDeviceEnumerator=__uuidof(IMMDeviceEnumerator);
IMMDeviceEnumerator*ptrEnumerator;
hr=CoCreateInstance(
CLSID_MMDeviceEnumerator,
NULL,
CLSCTX_ALL,
IID_IMMDeviceEnumerator,
(void**)&pEnumerator);
通过上述代码可以获取到一个 IMMDeviceEnumerator 对象。通过这个对象,客户端可以直接或者间接获取到 MMDevice API 中包括 IMMDevice,IMMDeviceCollection 以及音频端点设备状态更改的通知 IMMNotificationClient 在内的对象。
IMMDeviceCollection*pCollection=NULL;
hr=pEnumerator->EnumAudioEndpoints(
dataFlow, // data-flow direction (input parameter)
DEVICE_STATE_ACTIVE|DEVICE_STATE_DISABLED|DEVICE_STATE_UNPLUGGED,
&pCollection); // release interface when done
IMMDevice*pEndpoint = NULL;
hr=pCollection->Item(index, &pEndpoint); //device Index Value
通过 pCollection 和 pEndpoint 对象可以调用 IMMDeviceCollection 中的 GetCount 接口获取设备个数;调用 IMMDevice 的 GetId 获取终端端口的设备ID;如果需要获取设备名需要稍微复杂的操作,首先要通过 IMMDevice 的 OpenPropertyStore 接口获取一个 IPropertyStore 对象,通过 IPropertyStore 的 GetValue 来获取到设备名。音视频客户端通过这些方法就可枚举出当前 windows 电脑中存在的音频终端设备及信息。
2.2.2、打开指定设备
在枚举完设备后,在用户指定某个特定设备时,一般客户端会选择把系统默认设备当成采集/播放设备。对于默认设备,Core Audio 有一个特定接口去 打开默认设备,调用 IMMDeviceEnumerator 的 GetDefaultAudioEndpoint 即可。但是当用户指定到某一个特定设备时,通过通过 IMMDevice 的 Item 接口可打开用户指定的设备。
2.2.3 设备初始化
设备初始化是整个工作线程中一个重要环节,客户端能够在音视频频客户端和音频引擎(对于共享模式的流)或音频终端设备的硬件缓冲器(对于独占模式的流)之间创建和初始化一个音频流。需要先调用 IMMDevice 的 Activate 方法创建具有指定接口的 IAudioClient 对象。
constIIDIID_IAudioClient=__uuidof(IAudioClient);
IAudioClient*pAudioClient=NULL;
hr=pDevice->Activate(
IID_IAudioClient,
CLSCTX_ALL,
NULL,
(void**) &pAudioClient);
在获取到 IAudioClient 对象后,进行设备初始化,但在 Initialize 调用中,客户端需要为流指定共享或者独占模式,控制流创建的标志、音频数据格式、缓冲区大小和音频会话。音视频客户端一般会选用共享模式,采集和播放一般使用事件驱动的方式,音频数据格式可以使用 IAudioClient 的 GetMixFormat 接口去获取默认格式,但是实际上获取到的默认格式并不一定符合客户端所需要的设备格式参数,那么会遍历通道数、采样率,调用 IAudioClient 的 IsFormatSupported 接口查询出最适合的设备格式参数。
hr=pAudioClient->Initialize(
AUDCLNT_SHAREMODE_SHARED,
AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
hnsRequestedDuration,
0,
pwfx,
NULL);
2.3 Core Audio的音量管理
音频设备中的音量控制系统主要由 EndpointVolume API 提供。音量控制需要使用到 IAudioEndpointVolume 对象,该对象由 IMMDevice 接口获取。
IAudioCaptureClient*pCaptureClient=NULL;
IAudioEndpointVolume*pEndpointVolume=NULL;
hr=pEndpoint->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_ALL, NULL,
(void**)&pEndpointVolume);
通过 pEndpointVolume 对象可以处理音量控制,静音控制。
floatfLevel;
//Get Volume
pEndpointVolume->GetMasterVolumeLevelScalar(&fLevel);
//Set Volume
fLevel = 255.0;
pEndpointVolume->SetMasterVolumeLevelScalar(fLevel, NULL);
BOOLmute;
//Get mute state
pEndpointVolume->GetMute(&mute);
//Set mute state
mute=0;
pEndpointVolume->SetMute(mute, NULL);
2.4 Core Audio事件监听管理
2.4.1 设备事件监听主要是监听设备的插播消息,由 IMMDeviceEnumerator 调用 RegisterEndpointNotificationCallback 接口便可实现当设备状态出现变化时能通知到音视频客户端中。
IMMNotificationClient*pClient=NULL;
ptrEnumerator->RegisterEndpointNotificationCallback(pClient);
2.4.2 音量事件监听由 EndpointVolume 调用 RegisterControlChangeNotify 接口实现
IAudioEndpointVolumeCallback*pVolume=NULL;
pEndpointVolume->RegisterControlChangeNotify(pVolume);
2.5 Core Audio 线程模型与 Call-Flow
在设备初始化完成后,接下来就到了最重要的环节:采集/播放的数据的交互。但是数据应该如何进行交互的,采集播放实践中这么多的环节是如何建立线程模型?
2.5.1 线程模型
实时音视频中,需要得得一个实时,高效的采集/播放,为防止两者相互 block,所以一般在实时音视频中,会将采集和播放创建单独的线程,称为采集/播放线程。同时为了防止被其他线程占用资源,采集/播放线程优先级一般都会设置为最高级别。当然对于设备枚举、设备初始化等低密度操作,一般在工作线程完成。而音量管理以及事件监控,都是通过用户去操作的,会用一个用户线程去管理。
图三 各线程示意图
2.5.2 采集 Call-Flow
了解一下采集的流程图。
图四 采集线程流程图
在图中,可以看到,麦克风设备采集是由 event 事件来驱动的,在初始化设备后,会设置一个启动事件 SetEvent(startEvent) 启动麦克风采集,并且生成一个 IAudioCaptureClient 的对象,通过 IAudioCaptureClient 对象来调用接口获取麦克风数据。
//在工作线程获取IAudioCaptureClient的对象
IAudioCaptureClient*pCaptureClient=NULL;
hr=pAudioClient->GetService(__uuidof(IAudioCaptureClient),
(void**)&pCaptureClient);
//采集线程
//获取麦克风数据
hr=pCaptureClient->GetBuffer(
&pData, // packet which is ready to be read by used
&framesAvailable, // #frames in the captured packet (can be zero)
&flags, // support flags (check)
&recPos, // device position of first audio frame in data packet
&recTime); // value of performance counter at the time of recording
// the first audio frame
//处理数据
ProcessCaptureData(&pData);
//释放麦克风数据
DWORDdwFlags(0);
hr=_ptrCaptureClient->ReleaseBuffer(framesAvailable);
2.5.3 播放Call-Flow
音频播放的流程图如下:
图五 播放线程流程图
扬声器的播放也是通过 event 事件来驱动的,也会设置一个 startEvent 启动。与采集不同的地方是扬声器需要先获取到当前设备的缓存 buffer。如果缓存的 buffer 已经满了,那么设备不会再去要数据用于扬声器的播放。当设备缓存 buffer 不够时,会先获取一个设备指针,从远端传入的数据写入到指针指向的地址中,当缓存写满,扬声器就会播放出来。
//工作线程获取IAudioCaptureClient的对象。
IAudioCaptureClient*pRenderClient=NULL;
hr=pAudioClient->GetService(__uuidof(IAudioRenderClient),
(void**)&pRenderClient);
//播放线程
//获取当前的Padding缓存
UINT32padding=0;
hr=pRenderClient->GetCurrentPadding(&padding);
//获取扬声器设备指针
hr=pRenderClient->GetBuffer(playBlockSize, &pData);
//远端数据写入缓存中
RequestPlayoutData(&pData);
//释放扬声器数据
DWORDdwFlags(0);
hr=pRenderClient->ReleaseBuffer(playBlockSize, dwFlags);
#3
Core Audio 使用注意事项
3.1 windows 有着自己的计时时钟。采集过程中,由于时钟精度问题,采集 callback 的数据大小与恒定的 frameSize 有着一定的差异性。例如以 44100Hz 的采样率去采集,单位时钟内 callback 的数据大小为448,与恒定的441有差异。
3.2 一旦采集/播放线程被 block 了,将会导致线程处理时间变长,采集/播放取出的数据会产生断裂问题。拿播放为例,用户就会听到卡顿声。
3.3 使用 GetMixFormat方法 获取默认设备格式时,通常以 WAVEFORMATEX 的结构来指定格式。但是 WAVEFORMATEX 的结构有着一定的局限性,对于双通道以上,或者更高位深精度,或者新的压缩方案的一些设备格式,微软建议使用 WAVEFORMATEXTENSIBLE 可以获得更好的支持。由于某些设备驱动对 WAVEFORMATEX 结构和 WAVEFORMATEXTENSIBLE 结构调用 IsFormatSupported 会得到不同的结果。为了获取到可靠的设备格式,微软建议使用 IsFormatSupported 对 WAVEFORMATEX 格式和 WAVEFORMATEXTENSIBLE 格式都进行一次遍历。
3.4 音频设备中还有一些其他设置,比如 built-in AEC。built-in AEC 是使用编解码器 DMO 接口配置附加功能,DOM 可能会影响一些设备格式的支持。
相关引用:
1.《Practical Digital Audio for C++ Programmers》;
2.Core Audio APIs - Win32 apps ;
3.Core Audio APIs - Win32 apps ;
4.Configuring Codec DMOs - Win32 apps