对于Windows的音频采集来说,方法有很多。可以通过较为上层的多媒体框架去采集,如DirectShow、MediaFoundation这些;也可以通过较为底层的API去采集,如WASAPI(Windows Audio Session API)。这里我介绍的是使用WASAPI的方法,毕竟采集和渲染都可以在这里去实现了。
WASAPI的简单介绍
这里简单介绍一下使用这个API来采集需要了解的两个接口:
- IAudioClient:音频使用端,用于创建并初始化音频流
- IAudioCaptureClient:音频采集端,提供了采集音频的接口
独占模式和共享模式
我们先来看看Windows下的音频框架关系图:
对于混音,如果两条音频流的采样率不同的话,肯定要先对某些音频流去做重采样。当高采样率向低采样率转换时,就会发生精度的丢失,而对于独占模式来说,由于独占了一路音频流,因此无需进行这一步重采样的过程。因此,对于独占模式来说,一般音质效果肯定是要好一些的。
至于如何选择,还是得看自己的实际需求。对于音频采集来说,我们大部分情况下都会去选择共享模式,这样我们可以同时采集到游戏里的声音,音乐播放器所播放的声音等等。
采集模式
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字节,来表示这部分是没有声音的,然后再作为输入去混音即可。