WebRTC音频01 - 设备管理
WebRTC音频 02 - Windows平台设备管理(本文)
WebRTC音频 03 - 实时通信框架
WebRTC音频 04 - 关键类
WebRTC音频 05 - 音频采集编码
一、前言:
上一节讲了WebRtc如何去管理音频设备的,最终得出结论就是使用了ADM。但是ADM之下,Windows、Linux、mac又各不相同,这一节就分析下Windows平台是如何管理这些音频设备的。
也就是分析下AudioDeviceWindowsCore这个类都干了啥!
分析之前我们得先看下Core Audio,因为这是最新windows平台对音频设备控制的模块。我们AudioDeviceWindowsCore也主要是调用Core Audio的API和音频设备进行交互的。
二、Windows Core Audio APIs:
Core Audio APIs是Windows Vista(Windows NT 6.0)推出的一个通用音频架构(Universal Audio Architecture简称UAA)的API。这是一套全新的基于用户模式的音频组件。
官网:https://learn.microsoft.com/en-us/windows/win32/coreaudio/core-audio-apis-in-windows-vista
1、Core Audio特点:
低延迟、具备故障恢复的音频流;高可靠性(许多音频函数已从内核模式移到了用户模式);高安全性(受保护的音频内容的处理在安全、低权限的进程中进行);将特定的系统范围角色(控制台、多媒体和通信)分配给单独的音频设备;对用户直接操作的音频终结点设备(例如扬声器、耳机和麦克风)进行了软件抽象;2、Core Audio架构:
官网:https://learn.microsoft.com/en-us/windows/win32/coreaudio/user-mode-audio-components
可以看出,总共分了三层:用户层、内核层、硬件设备层;CoreAudio使用硬件设备总共分为两种模式:共享模式(Shared Mode)、独享模式(Exclusive Mode); 共享模式:通过Audio Service -> Audio Engine去访问,和其他应用一同共享扬声器;独享模式:直接通过驱动去访问; Core Audio APIs内部:(下面一小节会详细分析) MMDevice:对设备的一些控制,比如检测设备列表;WASAPI:session层api,可以提供硬件的回音消除、降噪这种3A功能,WASAPI通过提供低延迟的音频路径,帮助应用程序更好地管理音频格式转换、混音等功能;EndpointVolume:控制音量;3、COM组件:
我们前面说了COM组件,简单介绍下。
官网:https://learn.microsoft.com/zh-cn/windows/win32/com/com-technical-overview
COM组件,即Component Object Model(组件对象模型),是由微软开发的一种用于软件组件开发的平台独立的分布式对象模型。它是Windows环境中用于创建可重用软件组件的重要技术。记住它是C/S模型,组件注册到系统表里面之后,可以创建一个Client去访问组件。以下是COM组件的一些关键特性和用途:
关键特性和概念:
组件化:COM 鼓励将软件系统划分为独立的组件,每个组件可以独立开发、部署和维护。这些组件可以通过 COM 进行通信,实现功能的模块化和复用。接口:在 COM 中,组件之间的交互是通过接口实现的。每个 COM 组件都公开一组接口,其他组件可以通过这些接口调用组件提供的功能。GUID:GUID(Globally Unique Identifier)是 COM 中用于唯一标识组件、接口和类的标识符。每个 COM 接口和类都有一个唯一的 GUID。实例化:COM 组件通过实例化来创建对象的实例。通常使用CoCreateInstance
函数来实例化 COM 对象。生命周期管理:COM 使用引用计数来管理对象的生命周期。当对象不再被引用时,引用计数会减少,当引用计数为零时对象会被销毁。注册表:COM 组件通常会在系统的注册表中注册,以便其他组件能够找到并使用它们。跨进程通信:COM 允许不同进程中的组件进行通信,实现跨进程的功能调用和数据传输。 CoCreateInstance 函数:
CoCreateInstance
是用于在 COM 中创建组件实例的函数。通过该函数,可以创建指定 CLSID 的 COM 组件实例,并获取该组件支持的接口指针。 Activate方法:
Activate方法是IMMDevice接口的重要方法,用于激活并获取其他特定接口的实例。
通过Activate,应用程序可以获取如IAudioClient(用于WASAPI流处理)和IAudioEndpointVolume(用于音量控制)接口。
这样做的主要目的是让客户端和音频设备之间建立一个桥梁,以便管理和操控设备的各种功能。
方法原型:
HRESULT Activate( REFIID iid, DWORD dwClsCtx, PROPVARIANT *pActivationParams, VOID **ppInterface);
参数介绍:
iid
(REFIID):
IID_IAudioClient
(用于WASAPI音频流)和 IID_IAudioEndpointVolume
(用于音量控制)。 dwClsCtx
(DWORD):
定义接口应在哪个上下文中运行。一般来说,使用
CLSCTX_ALL
即表示允许任意上下文:
CLSCTX_INPROC_SERVER
:接口在同一进程中的DLL实现。CLSCTX_INPROC_HANDLER
:处理对象的接口。CLSCTX_LOCAL_SERVER
:接口在一个与客户端进程分离的本地服务器中实现。CLSCTX_REMOTE_SERVER
:接口在远程服务器上实现(通常不用于音频设备)。 pActivationParams
(PROPVARIANT)*:
此参数用于传递特定的激活属性和参数,目前对于音频设备通常设置为 NULL
,因为大多数音频接口不需要额外的激活参数。
ppInterface
(VOID)**:
Release
方法调用)。 小结:
Enumerator是AudioCore的第一个方法,通过Enumerator可以获取IMMDevice接口;
再通过IMMDevice的Activate方法,才能获取到WASAPI和EndpointVolume接口;
所有AudioCore的接口都是从Enumerator开始的;
例如:
HRESULT hr;IMyInterface* pMyInterface = nullptr;hr = myObject->Activate(__uuidof(CLSID_MyClass), nullptr, CLSCTX_INPROC_SERVER, __uuidof(IMyInterface), (void**)&pMyInterface);
1)DMO组件:
DMO(Digital Media Objects) 是一个接口标准,用于在Windows平台上表示数字媒体处理的对象。DMO用于音频或视频解码、效果处理等。已经被微软抛弃了,因为WebRTC使用了,我们得了解下,具体看官网:
https://learn.microsoft.com/zh-cn/windows/win32/directshow/directly-hosting-a-dmo
WebRtc主要用来进行回音消除了。
其中最重要的是IMediaObject :
IMediaObject 是 DMO 的核心接口之一,用于描述 DMO 对象的基本功能和行为。这个接口定义了一组方法,允许应用程序与 DMO 实例进行交互,设置参数、处理数据流并获取输出。
2)STA与MTA:
STA和MTA是COM对象在Windows系统中管理线程的一种方式。基于COM的程序需要选择适合其应用场景的线程模型(STA或MTA),
主要是管理COM组件在应用程序中的线程调度和并发控制;
STA(Single-Threaded Apartment):
是COM中用于线程管理的一种模型。在STA模型中,每个线程都有自己的组件实例,并且这些实例不是线程安全的。线程间的通信依赖于Windows消息。
适用于与用户界面交互的应用程序,因为Windows UI框架(如WinForms、WPF)使用STA模式确保线程间的安全性。
比如,COM组件就是个资源,创建com组件之后放入线程A的apartment(空间),其他线程B和C想使用这个COM组件,那么,你就得通过A来访问,具体实现方式就是放入A的线程消息队列,挨个处理;
MTA(Multi-Threaded Apartment):
MTA允许多个线程在同一时间访问多个COM对象。相较于STA,MTA提供了更高的并发性,但要求开发者自己管理对资源的同步。适合需要高性能、多线程调用的后端处理和高负载计算任务。3)Avrt库:
AVRT(Multimedia Class Scheduler Service) 是与音频/视频实时性能相关的库。AVRT用于提升多媒体应用的线程优先级,以确保在高负载下音频/视频的平滑流畅播放和低延迟处理。
4、Core Audio APIs:
对于我们WebRtc,主要用到的是 MMDevice 和 EndpointVolume 这两类API。(注意这俩不是API,是对API分类,里面包含很多API);
看看这里面的API都具备什么功能:
1)MMDevice:
MMDevice
(Multimedia Device Interface)主要负责枚举(找到并列出)和管理系统中的音频设备。它提供了一种与操作系统进行通信的方法,以获取当前连接的音频输入和输出设备。这可以包括耳机、扬声器、麦克风等。MMDevice
接口允许应用程序:
重要API:
IMMDevice:表示一个音频设备;IMMDeviceCollection:表示一个音频设备集合;IMMDeviceEnumerator:枚举音频设备; 可以获取 IMMDevice 接口;可以获取 IMMDeviceCollection 接口;创建 IMMDeviceEnumerator 接口的方法: IMMEndpoint:表示一个音频终端设备;2)EndpointVolume:
EndpointVolume
接口专门用于音频设备的音量管理。它允许应用程序控制和获取某个音频设备的音量级别和静音状态。主要功能包括:
3)使用方法:
比如,我们呼叫会议之前肯定先要枚举当前电脑的设备,然后从列表中选择一个进行通话,具体方法如下:
位于:AudioDeviceWindowsCore::CoreAudioIsSupported中:
IMMDeviceEnumerator* pIMMD(NULL); const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator); const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator); hr = CoCreateInstance( CLSID_MMDeviceEnumerator, // GUID value of MMDeviceEnumerator coclass NULL, CLSCTX_ALL, IID_IMMDeviceEnumerator, // GUID value of the IMMDeviceEnumerator // interface (void**)&pIMMD);
这里面使用CoCreateInstance创建了一个COM组件,CoCreateInstance的五个入参分别表示:
参数1:实例化的组件GUID(Globally Unique Identifier),代表MMDeviceEnumerator
类的 Class ID(com组件提前会向系统注册,通过GUID可以找到这个组件);参数2:这是指向 IUnknown
接口的指针,用于指示实例化的对象不会有外部引用计数器;参数3:这个参数指定了 CoCreateInstance
函数在实例化对象时应该使用的上下文。CLSCTX_ALL
表示在所有上下文中查找并连接到对象。参数4:这也是一个 GUID,代表 IMMDeviceEnumerator
接口的 Interface ID。在这里,IID_IMMDeviceEnumerator
是 IMMDeviceEnumerator
接口的唯一标识符,用于指示要实例化的对象要实现的接口是 IMMDeviceEnumerator
;参数5:这是一个用于接收接口指针的地址。pIMMD
是一个指向 IMMDeviceEnumerator
接口的指针变量的地址,通过这个地址,函数会将实例化的 IMMDeviceEnumerator
接口的指针保存到 pIMMD
变量中,以便后续使用。 总结一句话:告诉你枚举设备的这个 接口的Com组件id和方法id,你给我返回这个枚举接口实例;
4)IMMDeviceEnumerator方法:
前面我们知道 IMMDeviceEnumerator 是MMDevice的一个重要的方法;我们主要用来枚举设备,以及获取默认的设备,这两方法如下:
EnumAudioEndpoints:
用于枚举系统中的音频端点(音频设备)。
函数原型:
HRESULT EnumAudioEndpoints( EDataFlow dataFlow, DWORD dwStateMask, IMMDeviceCollection **ppDevices);
EDataFlow dataFlow
:指定要枚举的音频端点的数据流方向。可以是 eRender
(输出设备)或 eCapture
(输入设备)。
DWORD dwStateMask
:指定要检索的音频端点的状态。可以是 DEVICE_STATE_ACTIVE
(激活状态)等不同状态的组合。
IMMDeviceCollection **ppDevices
:指向 IMMDeviceCollection
接口指针的指针。这个接口表示找到的音频设备集合。
返回值:函数返回 HRESULT
类型的错误码,如果调用成功则返回 S_OK
;
GetDefaultAudioEndpoint:
用于获取系统中默认的音频输入或输出设备的 IMMDevice
接口实例。
函数原型:
HRESULT GetDefaultAudioEndpoint( EDataFlow dataFlow, ERole role, IMMDevice **ppEndpoint);
EDataFlow dataFlow
:指定要获取的默认音频端点的数据流方向。可以是 eRender
(输出设备)或 eCapture
(输入设备)。
ERole role
:指定要获取的音频端点的角色。可以是 eConsole
(通信设备)、eMultimedia
(多媒体设备)或 eCommunications
(通信设备)。
IMMDevice **ppEndpoint
:指向 IMMDevice
接口指针的指针。当函数成功调用后,将会保存默认音频设备的 IMMDevice
接口实例。
返回值:函数返回 HRESULT
类型的错误码,如果调用成功则返回 S_OK
。
三、AudioDeviceWindowsCore:
前面了解了Windows Core Audio APIs,我们就可以看懂AudioDeviceWindowsCore里面所有关于windows平台的一些操作了,否则云里雾里,极其痛苦。
之前介绍adm的文章中,我们介绍了AudioDeviceWindowsCore是什么时候被创建的,现在我们看看AudioDeviceWindowsCore构造函数里面做了什么。
代码路径:.\modules\audio_device\win\audio_device_core_win.cc
注意:非关键代码都删除了,否则非常庞大,影响阅读!!!
AudioDeviceWindowsCore::AudioDeviceWindowsCore() : _avrtLibrary(NULL), _winSupportAvrt(false), _comInit(ScopedCOMInitializer::kMTA), // 使用MTA(线程管理方式)创建了COM组件{ // 初始化COM组件 RTC_DCHECK(_comInit.Succeeded()); // Try to load the Avrt DLL if (!_avrtLibrary) { // 导入Avrt库 _avrtLibrary = LoadLibrary(TEXT("Avrt.dll")); if (_avrtLibrary) { // 拿到这三个方法的函数指针 _PAvRevertMmThreadCharacteristics = (PAvRevertMmThreadCharacteristics)GetProcAddress( _avrtLibrary, "AvRevertMmThreadCharacteristics"); _PAvSetMmThreadCharacteristicsA = (PAvSetMmThreadCharacteristicsA)GetProcAddress( _avrtLibrary, "AvSetMmThreadCharacteristicsA"); _PAvSetMmThreadPriority = (PAvSetMmThreadPriority)GetProcAddress( _avrtLibrary, "AvSetMmThreadPriority"); if (_PAvRevertMmThreadCharacteristics && _PAvSetMmThreadCharacteristicsA && _PAvSetMmThreadPriority) { // 这三个方法都获取成功了,标记个flag _winSupportAvrt = true; } } } // 存一些麦克风通道个数 _recChannelsPrioList[0] = 2; // stereo is prio 1 _recChannelsPrioList[1] = 1; // mono is prio 2 _recChannelsPrioList[2] = 4; // quad is prio 3 // 存一些扬声器的通道个数 _playChannelsPrioList[0] = 2; // stereo is prio 1 _playChannelsPrioList[1] = 1; // mono is prio 2 HRESULT hr; // 创建具有 IMMDeviceEnumerator 接口的COM对象,并返回函数指针 CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), reinterpret_cast<void**>(&_ptrEnumerator)); // 创建具有 IMediaObject 接口的COM对象,并返回函数指针 // webrtc只用DMO做了AEC(回音消除,并且这儿指的是硬件的回音消除) { IMediaObject* ptrDMO = NULL; hr = CoCreateInstance(CLSID_CWMAudioAEC, NULL, CLSCTX_INPROC_SERVER, IID_IMediaObject, reinterpret_cast<void**>(&ptrDMO)); _dmo = ptrDMO; SAFE_RELEASE(ptrDMO); }}
关键步骤我都写了注释,主要做了几件事:
初始化COM组件,并且线程管理方式是MTA方式;导入Avrt库;从Avrt库扩区几个关键方法的函数地址;(这种方式是否理解?和linux系统的dlsym类似)__uuidof
是一个编译器操作符,用于获取 COM 对象的 CLSID 或 IID。它接受一个 COM 对象或接口类型,返回其对应的 GUID;然后就是存了扬声器和麦克风的通道优先级,可以看出扬声器优先双声道,再单声道,麦克风就是2->1->4这样;最后就是创建具有 IMMDeviceEnumerator 接口的COM对象,并返回函数指针;创建具有 IMediaObject 接口的COM对象,并返回函数指针; 四、总结:
本文主要介绍了Windows平台的音频设备是如何管理和访问的,主要是对Core Audio的架构做了剖析,后续阅读webrtc代码时候,与平台相关的代码就不会迷茫了。