waveInAddBuffer死锁的大雷解决

项目场景:

从来没有一个bug让我这么抓狂,足足查了3天3夜,官方文档翻了一遍说的基本无用。具体项目就是使用waveIn系列函数获取windows系统麦克风数据,虽然windows上有好几种方法获取麦克风数据,我最终还是选择了它。


问题描述

我用异步回调函数方法来获取数据,当然还可以采用直接方法来获取数据,这里就不多说了,可以看下官方文档。回调部分类似下面这样:

cpp 复制代码
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 NDEBUG
            std::cout << "producer acquired" << std::endl;
#endif
            auto 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 NDEBUG
            std::cout << "producer released" << std::endl;
#endif
            pcm_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;
    }
}

真正的问题来了,正常使用肯定没问题,但是偏偏我的问题别人不一定遇到,我需要切换麦克风,也就是说我有一种需求电脑上同时连着几个麦克风,我需要根据场景切换到不同的麦克风上去。

不要怀疑我为什么会有多个麦克风,客户要求的,注意:好戏要登场了!

我获取设备的方法和别人一样,就像下面的代码:

cpp 复制代码
auto rc = waveInOpen(&hWaveIn, WAVE_MAPPER, &wfx, (DWORD_PTR) waveInProc, 0, CALLBACK_FUNCTION);

这是官方接口的写法,这么写在绝大多数场景下都是没问题的。问题出在什么地方呢?就是这个参数:WAVE_MAPPER,先看看官方的解释:

cpp 复制代码
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,可以通过下面的函数获取:

cpp 复制代码
    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为我要用的设备:

cpp 复制代码
        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代码可以这样写:

cpp 复制代码
                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的耗时Debug20ms左右,Release5-7ms左右,对时间要求高的童鞋可以再优化下。

还有一种方法稍微难一点,我没采用,我可以说下思路,感兴趣的同学可以尝试下,具体思路就是通过监控麦克风状态来决定操作,比如麦克风插入麦克风移除麦克风改变等等。下面贴出示例代码:

cpp 复制代码
#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 methods
    STDMETHODIMP 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 methods
    STDMETHODIMP 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 up
    pEnumerator->UnregisterEndpointNotificationCallback(pCallback);

    CoUninitialize();
    return 0;
}

每个方法名对应一个事件,你们自行钻研下吧,我用规避的方法就行了。

相关推荐
星释2 小时前
鸿蒙Flutter三方库适配指南: 05.使用Windows搭建开发环境
windows·flutter·harmonyos
炒茄子12 小时前
Windows:解决电脑开机解锁后黑屏但鼠标可见可移动的问题
windows·计算机外设
luyun02020218 小时前
流批了,pdf批量转excel
windows·pdf·excel·figma
vortex520 小时前
在 Windows 系统中安装 Oracle、SQL Server(MSSQL)和 MySQL
windows·oracle·sqlserver
路由侠内网穿透20 小时前
本地部署开源物联网平台 ThingsBoard 并实现外部访问( Windows 版本)
运维·服务器·windows·物联网·开源
Mr.Lu ‍1 天前
Windows开发,制作开发软件安装程序(二)
windows
skywalk81631 天前
windows装wsl ubuntu24.04 ,里面装qemu ,然后装mac os (windows也可以直接qemu安装macos)(未实践)
windows·ubuntu·macos·qemu
电脑小白技术1 天前
u盘安装系统提示“windows无法安装到这个磁盘,选中的磁盘具有gpt分区表”解决方法
windows·gpt·windows无法安装到磁盘
爱隐身的官人1 天前
Windows配置解压版MySQL5(免安装)
windows·mysql
JH30732 天前
10分钟理解泛型的通配符(extends, super, ?)
java·开发语言·windows