Windows平台下使用WASAPI进行音频数据采集

在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 <MMDeviceAPI.h>

1.1 创建 IMMDeviceEnumerator interface

ComPtr<IMMDeviceEnumerator> 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<IMMDevice>           device;
ComPtr<IAudioClient>        client;
ComPtr<IAudioCaptureClient> capture; //采集音频数据对象
ComPtr<IAudioRenderClient>  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<IPropertyStore> 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 <AudioClient.h>

程序可通过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<WAVEFORMATEX> 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<WAVEFORMATEX> wfex;
	HRESULT                    res;
	LPBYTE                     buffer;
	UINT32                     frames;
	ComPtr<IAudioClient>       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必须和GetBufferIAudioCaptureClient::ReleaseBuffer在同一线程中调用。

剩下就是音频数据的保存数据的处理。

写到最后

其中具体实现都是参考OBS的源码进行分析,obs还包括采集到的声卡数据和麦克风的数据重采样、混音等操作动作。这篇文章也鉴介其他大佬们的博客,也有自己再开发中遇到的问题做了总结等。最后想说微软的开发手册才是最全的,当然都是英文资料。

  • 6
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值