在Windows平台开发音视频的时候,常常需要对麦克风和扬声器的数据进行音频采集,这里简单记录一下大概流程和在实际过程中遇到的一些坑,如有表述错误地方请各位大佬在评论区指正。
MMDevice API获取设备
The Windows Multimedia Device (MMDevice) API enables audio clients to discover audio endpoint devices, determine their capabilities, and create driver instances for those devices.
Header file Mmdeviceapi.h defines the interfaces in the MMDevice API.
音频client利用MMDevice API来发现audio endpoint devices, 为devices创建驱动实例等。
头文件:
#include
1.1 创建 IMMDeviceEnumerator interface
ComPtr enumerator;
HRESULT res;
res = CoCreateInstance(__uuidof(MMDeviceEnumerator),
nullptr, CLSCTX_ALL,
__uuidof(IMMDeviceEnumerator),
(void**)enumerator.Assign());
if (FAILED(res))
throw HRError("Failed to create enumerator", res);
1.2 获取默认的设备GetDefaultAudioEndpoint
微软msdn地址https://msdn.microsoft.com/en-us/library/windows/desktop/dd371401(v=vs.85).aspx
ComPtr device;
ComPtr client;
ComPtr capture; //采集音频数据对象
ComPtr render; //声音渲染对象
HRESULT res;
if (isDefaultDevice) {
res = enumerator->GetDefaultAudioEndpoint(
isInputDevice ? eCapture : eRender,
//isInputDevice ? eCommunications : eConsole,
isInputDevice ? eMultimedia : eConsole,
device.Assign());
} else {
wchar_t *w_id;
os_utf8_to_wcs_ptr(device_id.c_str(), device_id.size(), &w_id);
res = enumerator->GetDevice(w_id, device.Assign());
bfree(w_id);
}
其中eCapture表示麦克风 eRender表示扬声器;在GetDefaultAudioEndpoint中第二个参数在msdn上也有说明,但是在win7/8里面如果麦克风设置成eCommunications角色,在你进行采集数据的时候系统会认为你正在通讯,所以会把音量降低80%,这是一个很操蛋的角色,但是如果实际场景需要还是试着用这个角色来看效果。其他角色MSDN上都有具体说明。
在多设备的时候也可以自行选择哪个设备。
1.3 获取设备名称
string device_name;
ComPtr store;
HRESULT res;
if (SUCCEEDED(device->OpenPropertyStore(STGM_READ, store.Assign()))) {
PROPVARIANT nameVar;
PropVariantInit(&nameVar);
res = store->GetValue(PKEY_Device_FriendlyName, &nameVar);
if (SUCCEEDED(res) && nameVar.pwszVal && *nameVar.pwszVal) {
size_t len = wcslen(nameVar.pwszVal);
size_t size;
size = os_wcs_to_utf8(nameVar.pwszVal, len,
nullptr, 0) + 1;
device_name.resize(size);
os_wcs_to_utf8(nameVar.pwszVal, len,&device_name[0], size);
}
}
WASAPI进行音频数据采集
The Windows Audio Session API (WASAPI) enables client applications to manage the flow of audio data between the application and an audio endpoint device.
Header files Audioclient.h and Audiopolicy.h define the WASAPI interfaces.
头文件:
#include
程序可通过audio engine,以共享模式访问audio endpoint device(比如麦克风 或Speakers)。
audio engine在endpoint buffer和endpoint device之间传输数据。
当播放音频数据时,程序向rendering endpoint buffer周期性写入数据。
当采集音频数据时,程序从capture endpoint buffer周期性读取数据。
使用WASAPI的几个重要函数:
1.IMMDevice::Activate
IMMDevice::Activate来获取an audio endpoint device的IAudioClient interface引用。
1)先获取一个device,比如麦克风设备
2)调用Activate激活该麦克风的音频采集接口
device->Activate(__uuidof(IAudioClient), CLSCTX_ALL,nullptr, (void**)client.Assign());
2.IAudioClient::Initialize
IAudioClient::Initialize用来在endpoint device初始化流。
通用格式:
CoTaskMemPtr wfex;
HRESULT res;
DWORD flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK;
res = client->GetMixFormat(&wfex);
if (FAILED(res))
throw HRError("Failed to get mix format", res);
InitFormat(wfex);
if (!isInputDevice)
flags |= AUDCLNT_STREAMFLAGS_LOOPBACK;
res = client->Initialize(AUDCLNT_SHAREMODE_SHARED, flags,BUFFER_TIME_100NS, 0, wfex, nullptr);
我们先来看看Windows下的音频框架关系图:
在Render设备进行Initialize第一个参数是分为独占模式和共享模式,如上图可知Exclusive Mode直接和音频驱动直连,而Shared Mode 需要一个Audio Engine 这样做的好处是可以把好多应用的声音采集进行Mix,这样你就可以采集到多处声音。当然在Mix会做重采样动作,在高采样率转低采样的时候会有精度的丢失。
在initialize中的第三个参数是 100ns(nanosecond) 为单位,纳秒:时间单位。一秒的十亿分之一
1秒=1000毫秒; 1毫秒=1000微秒; 1微秒=1000纳秒
其中程序设置的BUFFER_TIME_100NS=(5*10000000)
其中:
AUDCLNT_STREAMFLAGS_LOOPBACK 表示 音频engine会将rending设备正在播放的音频流, 拷贝一份到音频的endpoint buffer这样的话,WASAPI client可以采集到the stream.
如果AUDCLNT_STREAMFLAGS_LOOPBACK被设置,IAudioClient::Initialize会尝试在rending设备开辟一块capture buffer。
AUDCLNT_STREAMFLAGS_LOOPBACK只对rending设备有效,Initialize仅在AUDCLNT_SHAREMODE_SHARED时才可以使用, 否则Initialize会失败。
AUDCLNT_STREAMFLAGS_EVENTCALLBACK 表示当audio buffer数据就绪时,会给系统发个信号,也就是事件触发。
在wsapi中采集到的PCM数据总是float
obs在采集声卡声音对render对象做了一次初始化:
CoTaskMemPtr wfex;
HRESULT res;
LPBYTE buffer;
UINT32 frames;
ComPtr client;
res = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL,
nullptr, (void**)client.Assign());
if (FAILED(res))
throw HRError("Failed to activate client context", res);
res = client->GetMixFormat(&wfex);
if (FAILED(res))
throw HRError("Failed to get mix format", res);
res = client->Initialize(
AUDCLNT_SHAREMODE_SHARED, 0,
BUFFER_TIME_100NS, 0, wfex, nullptr);
if (FAILED(res))
throw HRError("Failed to get initialize audio client", res);
/* Silent loopback fix. Prevents audio stream from stopping and */
/* messing up timestamps and other weird glitches during silence */
/* by playing a silent sample all over again. */
res = client->GetBufferSize(&frames);
if (FAILED(res))
throw HRError("Failed to get buffer size", res);
res = client->GetService(__uuidof(IAudioRenderClient),
(void**)render.Assign());
if (FAILED(res))
throw HRError("Failed to get render client", res);
res = render->GetBuffer(frames, &buffer);
if (FAILED(res))
throw HRError("Failed to get buffer", res);
memset(buffer, 0, frames*wfex->nBlockAlign);
render->ReleaseBuffer(frames, 0);
3.IAudioClient::GetService
初始化流之后,可调用IAudioClient::GetService来获取其它 WASAPI interfaces的引用。
HRESULT res = client->GetService(__uuidof(IAudioCaptureClient),
(void**)capture.Assign());
if (FAILED(res))
throw HRError("Failed to create capture context", res);
res = client->SetEventHandle(receiveSignal);//设置信号
if (FAILED(res))
throw HRError("Failed to set event handle", res);
captureThread = CreateThread(nullptr, 0,
WASAPISource::CaptureThread, this,
0, nullptr);
if (!captureThread.Valid())
throw "Failed to create capture thread";
client->Start();
active = true;
client->SetEventHandle(receiveSignal) 用于client通知有音频数据 因为在client初始化的时候设置了AUDCLNT_STREAMFLAGS_EVENTCALLBACK
4.IAudioClient::Start
start之后就开始使用采集对象来进行接受数据,设置一个接受数据的线程,
CreateThread(nullptr, 0,WASAPISource::CaptureThread, this,0, nullptr);
5.IAudioCaptureClient::GetNextPacketSize
官方解释
The GetNextPacketSize method retrieves the number of frames in the next data packet in the capture endpoint buffer.
这里有两个注意的:
(1) 单位为audio frame。
(2) 注意是采集buffer(capture endpoint buffer)
仅在共享模式下生效,独占模式下无效。
在调用GetBuffer之前,可调用GetNextPacketSize来获取下一个数据包的音频帧个数。
6.IAudioCaptureClient::GetBuffer
最重要的函数。用于获取capture endpoint buffer中下一个数据包的指针。
HRESULT GetBuffer(
[out] BYTE **ppData,
[out] UINT32 *pNumFramesToRead,
[out] DWORD *pdwFlags,
[out] UINT64 *pu64DevicePosition,
[out] UINT64 *pu64QPCPosition
);
使用方法:
HRESULT res;
LPBYTE buffer;
UINT32 frames;
DWORD flags;
UINT64 pos, ts;
UINT captureSize = 0;
while (true) {
res = capture->GetNextPacketSize(&captureSize);
if (FAILED(res)) {
if (res != AUDCLNT_E_DEVICE_INVALIDATED)
blog(LOG_WARNING,
"[WASAPISource::GetCaptureData]"
" capture->GetNextPacketSize"
" failed: %lX", res);
return false;
}
if (!captureSize)
break;
res = capture->GetBuffer(&buffer, &frames, &flags, &pos, &ts);
if (FAILED(res)) {
if (res != AUDCLNT_E_DEVICE_INVALIDATED)
blog(LOG_WARNING,
"[WASAPISource::GetCaptureData]"
" capture->GetBuffer"
" failed: %lX", res);
return false;
}
obs_source_audio data = {};
data.data[0] = (const uint8_t*)buffer;
data.frames = (uint32_t)frames;
data.speakers = speakers;
data.samples_per_sec = sampleRate;
data.format = format;
data.timestamp = useDeviceTiming ? ts*100 : os_gettime_ns();
if (!useDeviceTiming)
data.timestamp -= (uint64_t)frames * 1000000000ULL /
(uint64_t)sampleRate;
obs_source_output_audio(source, &data);
capture->ReleaseBuffer(frames);
}
return true;
这个方法的最后一个参数可以作为音频数据的时间戳。
GetNextPacketSize必须和GetBuffer及IAudioCaptureClient::ReleaseBuffer在同一线程中调用。
剩下就是音频数据的保存数据的处理。
其中具体实现都是参考OBS的源码进行分析,obs还包括采集到的声卡数据和麦克风的数据重采样、混音等操作动作。这篇文章也鉴介其他大佬们的博客,也有自己再开发中遇到的问题做了总结等。最后想说微软的开发手册才是最全的,当然都是英文资料。