在音视频通信解决流程中,音频方面最根本的无外乎就是音频的采集和播放。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