谈谈Windows下的音频采集

5,660 阅读6分钟

  对于Windows的音频采集来说,方法有很多。可以通过较为上层的多媒体框架去采集,如DirectShow、MediaFoundation这些;也可以通过较为底层的API去采集,如WASAPI(Windows Audio Session API)。这里我介绍的是使用WASAPI的方法,毕竟采集和渲染都可以在这里去实现了。

WASAPI的简单介绍

  这里简单介绍一下使用这个API来采集需要了解的两个接口:

  • IAudioClient:音频使用端,用于创建并初始化音频流
  • IAudioCaptureClient:音频采集端,提供了采集音频的接口

独占模式和共享模式

  我们先来看看Windows下的音频框架关系图:

从图中我们可以看到,音频流会有两条路,一条是走向shared mode,一条则是exclusive mode,分别代表着共享模式和独占模式。对于独占模式这条路线,我们可以看到它直接连到了Audio Driver,也就是音频驱动中了,而共享模式还需要经过一个Audio Engine的过程,这个Audio Engine最终会把当前的音频流,还有其它来自其它应用的音频流进行混音,最后才传送到Audio Driver中去。
  对于混音,如果两条音频流的采样率不同的话,肯定要先对某些音频流去做重采样。当高采样率向低采样率转换时,就会发生精度的丢失,而对于独占模式来说,由于独占了一路音频流,因此无需进行这一步重采样的过程。因此,对于独占模式来说,一般音质效果肯定是要好一些的。
  至于如何选择,还是得看自己的实际需求。对于音频采集来说,我们大部分情况下都会去选择共享模式,这样我们可以同时采集到游戏里的声音,音乐播放器所播放的声音等等。

采集模式

  WASAPI有两种采集模式,一种是输入设备的采集,如带麦克风的耳机,麦克风这些;还有一种则是声卡输出的采集,也称作loop back mode。由于两种采集模式是分开的,如果需要同时听到输入设备的声音和声卡输出的声音,则需要对采集到的两路音频流进行混音。

WASAPI的简单使用

获取音频设备枚举实例:

  首先,我们需要通过COM接口来获取音频设备枚举实例,代码如下:

HRSULT hr;
IMMDeviceEnumerator* pEnumerator = NULL;
hr = CoCreateInstance(CLSID_MMDeviceEnumerator, NULL, CLSCTX_ALL, IID_PPV_ARGS(&pEnumerator));

枚举音频设备

  枚举音频设备,我们可以通过EnumAudioEndPoints来一个个进行枚举并选择合适的,但更多情况下我们是直接使用GetDefaultAudioEndpoint来获取一个默认的,且处于active状态的音频设备。代码如下:

IMMDevice* pDevice = NULL;
hr = pEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &pDevice);

  获取完音频设备之后,我们可以直接调用其activate方法(类似于工厂模式的用法),来获取IAudioClient:

IAudioClient* pAudioClient = NULL;
hr = pDevice->Activate(IID_IAudioClient, CLSCTX_ALL, NULL,(void**) &pAudioClient);

获取音频格式

  由于我们使用的是共享模式,因此我们需要获得Audio Engine的混音格式,通过调用GetMixFormat来获得,代码如下:

WAVEFORMATEX* pwfx = NULL;
hr = pAudioClient->GetMixFormat(&pwfx);

其中,WAVEFORMATEX的结构体就包含了我们所需要的混音格式,里面有音频格式,声道数,采样率,样本大小等信息。其中音频格式还需要将这个结构体扩展(也就是强转)成WAVEFORMATEXTENSIBLE这个结构体才能获得,这部分具体可参阅文档。

初始化

  接下来就是初始化了,即调用Initialize接口。这里面的参数还是需要特别注意的,我们先来看看有哪些参数:

HRESULT Initialize(
  AUDCLNT_SHAREMODE  ShareMode,
  DWORD              StreamFlags,
  REFERENCE_TIME     hnsBufferDuration,
  REFERENCE_TIME     hnsPeriodicity,
  const WAVEFORMATEX *pFormat,
  LPCGUID            AudioSessionGuid
);

  对于第一个参数,之前我们也说了,我们是在共享模式下进行采集的,因此这里填AUDCLNT_SHAREMODE_SHARED。
  对于第二个参数,如果是初始化输入设备的采集的话,直接填NULL即可。如果是采集声卡输出的,即loop back模式的话,则需要填入AUDCLNT_STREAMFLAGS_LOOPBACK。
  第三个是我们的缓存大小,对于采集到的音乐,WASAPI会帮我们放到这个缓存中。至于要设置成多大,其实一般是根据你的采集周期来确定的,你可以根据实际情况简单计算一下,设置成稍微比实际的大一点就好,这样可以避免溢出导致数据丢失的情况。
  第四个参数是用于独占模式的,我们这里用不到,直接填0。
  第五个参数就是我们之前获得的混音格式。
  最后一个参数是指定音频Session,这里我们不关心这个,直接填NULL,让它自己帮我们创建一个Session,并把我们的音频流加入到其中。

获取音频采集接口,开始采集

  最后一步便是获取我们的采集接口,并开始采集,代码如下:

IAudioCaptureClient* pCaptureClient = NULL;
hr = pAudioClient->GetService(
	IID_IAudioCaptureClient,
	(void**)&pCaptureClient);
hr = pAudioClient->Start();

获取采集到的数据

  我们通过GetNextPacketSize来判断下一个数据包的大小,如果不为0的话,我们就通过GetBuffer来获取一个数据包,依此类推,直到GetNextPacketSize返回的大小为0,说明我们这一个周期的数据已经获取完了,此时我们可以休眠一个周期,然后再去获取数据。代码如下:

while(1)
{
    //休眠一个周期
    Sleep(duration);
    hr = pCaptureClient->GetNextPacketSize(&packetLength);
    while(packetLength)
    {
    	hr = pCaptureClient->GetBuffer(
	    &pData,
	    &numFramesAvailable,
	    &flags, NULL, NULL);
	    //DoSomething()....
	    hr = pCaptureClient->ReleaseBuffer(numFramesAvailable);
	    hr = pCaptureClient->GetNextPacketSize(&packetLength);
    }
}

停止采集

  结束采集后调用stop方法即可:

hr = pAudioClient->Stop();

输入设备与loop back混音

  如果要同时听到人说话的声音和电脑里的声音,就需要对这两路音频流进行混音,即对两路音频流按一定的比例进行叠加。这一部分可以由第三方的媒体库来完成,如ffmpeg,我们可以利用amix滤镜来实现。如果要调整音量的话,可以加上volume滤镜来去调节。具体细节不属于本节范畴,因此就不展开细讲了。
  需要注意的是,当电脑的中没有产生声音时,我们是获取不到音频数据的,也就是GetNextPacketSize返回的长度是0.对于混音来说,所传入的音频数据的长度应该是要对等的,传入采集到的输入设备的一秒钟的音频数据,对应loop back来说对应的肯定也得是一秒钟的音频数据。因此,对于loop back的采集来说,若此刻只有麦克风的输入,我们可以适当的填充一些silence字节,来表示这部分是没有声音的,然后再作为输入去混音即可。