|
楼主 |
发表于 2021-6-21
|
在这一章,已经看到了音频流这个短语,音频流指的是应用程序和音频终端设备之间的一个连接。
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 |
|