项目场景:
从来没有一个bug让我这么抓狂,足足查了3天3夜,官方文档翻了一遍说的基本无用。具体项目就是使用waveIn系列函数获取windows系统麦克风数据,虽然windows上有好几种方法获取麦克风数据,我最终还是选择了它。
问题描述
我用异步回调函数方法来获取数据,当然还可以采用直接方法来获取数据,这里就不多说了,可以看下官方文档。回调部分类似下面这样:
void CALLBACK waveInProc(HWAVEIN hwi, UINT uMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2) {if (uMsg == WIM_DATA) {if (pcm_mutex.try_lock()) {
#ifndef NDEBUGstd::cout << "producer acquired" << std::endl;
#endifauto pwh = (LPWAVEHDR) dwParam1;if (pwh->lpData && pwh->dwBytesRecorded > 0) {pcm_str.assign(pwh->lpData, pwh->dwBytesRecorded);waveInAddBuffer(hwi, pwh, sizeof(WAVEHDR));} else {std::cerr << "wave data invalid" << std::endl;}
#ifndef NDEBUGstd::cout << "producer released" << std::endl;
#endifpcm_mutex.unlock();conn.notify_one();this_thread::sleep_for(std::chrono::microseconds{1});}} else if (uMsg == WIM_CLOSE) {std::cout << "wave close" << std::endl;} else if (uMsg == WIM_OPEN) {std::cout << "wave open" << std::endl;} else {std::cerr << "unknown option" << std::endl;}
}
真正的问题来了,正常使用肯定没问题,但是偏偏我的问题别人不一定遇到,我需要切换麦克风,也就是说我有一种需求电脑上同时连着几个麦克风,我需要根据场景切换到不同的麦克风上去。
不要怀疑我为什么会有多个麦克风,客户要求的,注意:好戏要登场了!
我获取设备的方法和别人一样,就像下面的代码:
auto rc = waveInOpen(&hWaveIn, WAVE_MAPPER, &wfx, (DWORD_PTR) waveInProc, 0, CALLBACK_FUNCTION);
这是官方接口的写法,这么写在绝大多数场景下都是没问题的。问题出在什么地方呢?就是这个参数:WAVE_MAPPER
,先看看官方的解释:
MMRESULT waveInOpen(LPHWAVEIN phwi,UINT uDeviceID,LPCWAVEFORMATEX pwfx,DWORD_PTR dwCallback,DWORD_PTR dwInstance,DWORD fdwOpen
);
uDeviceID
要打开的波形音频输入设备的标识符。 它可以是设备标识符,也可以是开放波形音频输入设备的句柄。 可以使用以下标志而不是设备标识符。
WAVE_MAPPER
函数选择能够以指定格式录制的波形音频输入设备。
所以如果你使用了WAVE_MAPPER
这个值,当你正在获取声音回调的时候,你突然切换麦克风
(或取消麦克风权限
),我们的主角来了waveInAddBuffer
就会很大概率进入死锁状态(不是必然
),是不是感觉很诡异。这跟很多其他网友说的waveInReset
进入死锁状态是一个性质,这曾经让我一度认为WAVE_MAPPER
这个值是有bug的。
原因分析:
只能说不是所有人都面对我这种场景,如果你是单麦克风按照我那种写法我是没有遇到bug。
简单分析下,还是要从那个回调函数着手,首先你调用waveInOpen
函数才会触发回调函数里的 WIM_OPEN
事件,同样你调用waveInClose
才会触发回调函数里的WIM_CLOSE
,前提是这两个函数必须执行成功才行,他们俩是有返回值的。
然后,其他的情况就是有数据上来的时候会触发WIM_DATA
事件,问题就出在这里,当你一直接收WIM_DATA
事件的时候突然切换麦克风(或取消麦克风权限,Windows10和Windows11有麦克风权限设置,Windows7好像没有)
,没有触发WIM_CLOSE
事件,因为你确实没手动调waveInClose
函数,最后一个Buffer发来的时候我无法判断当前的麦克风状态,waveInAddBuffer
函数将有概率进入假死状态
。
我分析,如果我收到了数据说明下面的锁已经解除了,这就跟生产者和消费者的模型是一样的,那么为什么会报错呢,原因很可能是Handle
的问题,就是说持有音频设备的句柄进入了不确定状态,有点像你正在往硬盘里写东西突然硬盘被人拔掉一样,我甚至怀疑是底层的bug,毕竟Windows11的状态大家都了解。
我就不说大话了,有时间我会向巨硬询问下的,我虽然没有100%确定问题,但是肯定和这个有关系。神奇的是我想到了解决的方法,或者说规避的方案,请看解决方案
。
解决方案:
还是要着眼于WAVE_MAPPER
这个参数本身,我们不接受它的建议,我们传入自己的值。每个麦克风设备都有自己的ID和Name,可以通过下面的函数获取:
UINT numDevs = waveInGetNumDevs();WAVEINCAPS wic;std::cout << "Number of input devices: " << numDevs << std::endl;for (UINT i = 0; i < numDevs; ++i) {if (waveInGetDevCaps(i, &wic, sizeof(WAVEINCAPS)) == MMSYSERR_NOERROR) {std::wcout << L"Device ID: " << i << std::endl;std::wcout << L"Device Name: " << wic.szPname << std::endl;}}
然后你根据看下自己电脑上大概有多少个设备,光这点还不够,我观察0
是默认设备
,请看下图:
你勾选了谁,谁的设备ID
就变成了0
,这就好办了,我只要手动选择想用的麦克风就可以了。然后我用下面的函数永久指定0
为我要用的设备:
auto rc = waveInOpen(&hWaveIn, 0, &wfx, (DWORD_PTR) waveInProc, 0, CALLBACK_FUNCTION);if (rc) {std::cerr << "waveInOpen failed: " << rc << std::endl;goto NONE;}
注意:当你勾选默认麦克风时候,重启电脑也不会重置,前提是这个麦克风必须一直处于可用状态,你不能把它拔掉或禁用。另外,除了0以外其他的设备排序是不固定的,不能想当然的认为是UI上的排序!
这个问题解决了就好办了,我可以在接收线程设置超时就行了,比如3秒或5秒没有收到数据大概率是麦克风改变了或挂掉了,也有可能是硬件问题。正常取一个buffer也就是最多几十毫秒(和硬件性能有关系
),所以3-5秒已经很长了,我测试下来是没有问题的。借助condition_veriable
代码可以这样写:
std::unique_lock<std::mutex> lck(pcm_mutex);auto status = conn.wait_for(lck, std::chrono::milliseconds{Config::recv_data_timeout},[]() { return !pcm_str.empty(); });//防止伪唤醒if(status){//正常流程}else{//异常处理}
我测试下来conn.wait_for
的耗时Debug
在20ms
左右,Release
在5-7ms
左右,对时间要求高的童鞋可以再优化下。
还有一种方法稍微难一点,我没采用,我可以说下思路,感兴趣的同学可以尝试下,具体思路就是通过监控麦克风状态来决定操作,比如麦克风插入
、麦克风移除
、麦克风改变
等等。下面贴出示例代码:
#include <windows.h>
#include <iostream>
#include <mmdeviceapi.h>
#include <audiopolicy.h>
#include <atlbase.h>#pragma comment(lib, "ole32.lib")
#pragma comment(lib, "avrt.lib")class DeviceNotificationCallback : public IMMNotificationClient {
public:// Implement required methodsSTDMETHODIMP OnDeviceStateChanged(LPCWSTR deviceId, DWORD newState) override {std::wcout << L"Device state changed: " << deviceId << std::endl;return S_OK;}STDMETHODIMP OnDeviceAdded(LPCWSTR deviceId) override {std::wcout << L"Device added: " << deviceId << std::endl;return S_OK;}STDMETHODIMP OnDeviceRemoved(LPCWSTR deviceId) override {std::wcout << L"Device removed: " << deviceId << std::endl;return S_OK;}STDMETHODIMP OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDefaultDeviceId) override {std::wcout << L"Default device changed." << pwstrDefaultDeviceId << std::endl;return S_OK;}STDMETHODIMP OnPropertyValueChanged(LPCWSTR deviceId, const PROPERTYKEY key) override {std::wcout << L"Property value changed: " << deviceId << std::endl;return S_OK;}// Unused methodsSTDMETHODIMP QueryInterface(REFIID riid, void **ppvObject) override {if (riid == __uuidof(IUnknown) || riid == __uuidof(IMMNotificationClient)) {*ppvObject = static_cast<IMMNotificationClient *>(this);AddRef();return S_OK;}return E_NOINTERFACE;}STDMETHODIMP_(ULONG) AddRef() override {return InterlockedIncrement(&m_refCount);}STDMETHODIMP_(ULONG) Release() override {ULONG refCount = InterlockedDecrement(&m_refCount);if (refCount == 0) {delete this;}return refCount;}private:LONG m_refCount = 1;
};int main() {CoInitialize(nullptr);CComPtr<IMMDeviceEnumerator> pEnumerator;CComPtr<DeviceNotificationCallback> pCallback;HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER,IID_PPV_ARGS(&pEnumerator));if (FAILED(hr)) {std::cerr << "Failed to create device enumerator. Error code: " << hr << std::endl;return -1;}pCallback = new DeviceNotificationCallback();hr = pEnumerator->RegisterEndpointNotificationCallback(pCallback);if (FAILED(hr)) {std::cerr << "Failed to register endpoint notification callback. Error code: " << hr << std::endl;return -1;}std::cout << "Monitoring device changes. Press Enter to exit." << std::endl;std::cin.get();// Clean uppEnumerator->UnregisterEndpointNotificationCallback(pCallback);CoUninitialize();return 0;
}
每个方法名对应一个事件,你们自行钻研下吧,我用规避的方法就行了。