如何以编程方式启用 sidetone/microphone 直通

How to enable sidetone/microphone pass-thru programmatically

对于我当前的项目,我正在用 C++ 实现一个本机库,我将通过 JNA 访问它,这个项目是一个低延迟通信模拟器。为了模仿模拟器所基于的硬件,需要在传输时启用侧音。 当然 JAVA 声音很难实现接近零延迟(我们能得到的最好是 ~120ms),为了保持可理解性,我们需要侧音的延迟接近零。幸运的是,在 Windows 中似乎有一种方法可以收听 usb 耳机的麦克风,它可以产生完美的侧音。

音频属性 -> 播放 -> 耳机 -> 属性 -> 级别

An example of what I mean here

(请注意,这与 'listen to this device' 功能不同,后者会产生非常糟糕的延迟)

我一直在使用 Core Audio API 的 MSDN 示例,并且能够查询设备并获取它们的频道、音量级别、静音设置等,但麦克风级别 mute/unmute 似乎甚至无法从核心音频 api 访问。

我的问题是:有没有办法以编程方式与 USB 耳机的麦克风 level/mute 设置交互?

我们的模拟器是标准化的,因此我们不必担心支持多种耳机(目前有 2 种)。

解决这个问题的关键是向后走设备拓扑树,直到找到负责设置侧音静音属性的部分。因此,在我的 CPP 项目中,我使用多种方法协同工作来确定我在拓扑树中的哪个位置寻找 SuperMix 部分。

SuperMix 似乎是侧音的通用名称,至少我们支持的两款耳机都在使用。两种耳机的树是相同的,您的里程可能会有所不同。这就是上述 WalkTreeBackwardsFromPart 示例的输出结果(参见 this answer

Part Name: SuperMix
    Part Name: Volume
        Part Name: Mute

这是我修改后的 WalkTreeBackwardsFromPart 版本,出于所有意图和目的,它只是检查我们当前正在查看的部分是否是 SuperMix 以及该部分的直接 child 是否是体积节点,这是为了防止错误分配,因为我发现我们的耳机通常有两个节点,称为 SuperMix,唯一的区别是我们想要的节点有一个音量节点 child.

HRESULT Sidetone::WalkTreeBackwardsFromPart(IPart *part) {

    HRESULT hr;

    if (wcscmp(this->getPartName(part), L"SuperMix") == 0 && this->treePeek(part, L"Volume")){

        this->superMix = part;

        IPart** superMixChildren = this->getChildParts(part);
        int nSuperMixChildren = sizeof(superMixChildren) / sizeof(superMixChildren[0]);
        if (nSuperMixChildren > 0){

            for (int i = 0; i < nSuperMixChildren; i++){

                if (wcscmp(this->getPartName(superMixChildren[i]), L"Volume") == 0){

                    this->volumeNode = this->getIPartAsIAudioVolumeLevel(superMixChildren[i]);
                    if (this->volumeNode != NULL){

                        IPart** volumeNodeChildren = this->getChildParts(superMixChildren[i]);
                        int nVolumeNodeChildren = sizeof(volumeNodeChildren) / sizeof(volumeNodeChildren[0]);
                        if (nVolumeNodeChildren > 0){

                            for (int j = 0; j < nVolumeNodeChildren; j++){

                                if (wcscmp(this->getPartName(volumeNodeChildren[j]), L"Mute") == 0){

                                    this->muteNode = this->getIPartAsIAudioMute(volumeNodeChildren[j]);
                                    break;
                                }
                            }
                        }
                    }
                    break;
                }
            }
        }
        delete[] superMixChildren;


        this->muteNode; // = someotherfunc();
        this->superMixFound = true;
        return S_OK;

    } else if(superMixFound == false){

        IPartsList *pIncomingParts = NULL;
        hr = part->EnumPartsIncoming(&pIncomingParts);
        if (E_NOTFOUND == hr) {
            // not an error... we've just reached the end of the path
            //printf("%S - No incoming parts at this part: 0x%08x\n", this->MSGIDENTIFIER, hr);
            return S_OK;
        }
        if (FAILED(hr)) {
            printf("%S - Couldn't enum incoming parts: hr = 0x%08x\n", this->MSGIDENTIFIER, hr);
            return hr;
        }
        UINT nParts = 0;
        hr = pIncomingParts->GetCount(&nParts);
        if (FAILED(hr)) {
            printf("%S - Couldn't get count of incoming parts: hr = 0x%08x\n", this->MSGIDENTIFIER, hr);
            pIncomingParts->Release();
            return hr;
        }

        // walk the tree on each incoming part recursively
        for (UINT n = 0; n < nParts; n++) {
            IPart *pIncomingPart = NULL;
            hr = pIncomingParts->GetPart(n, &pIncomingPart);
            if (FAILED(hr)) {
                printf("%S - Couldn't get part #%u (0-based) of %u (1-basedSmile hr = 0x%08x\n", this->MSGIDENTIFIER, n, nParts, hr);
                pIncomingParts->Release();
                return hr;
            }

            hr = WalkTreeBackwardsFromPart(pIncomingPart);
            if (FAILED(hr)) {
                printf("%S - Couldn't walk tree on part #%u (0-based) of %u (1-basedSmile hr = 0x%08x\n", this->MSGIDENTIFIER, n, nParts, hr);
                pIncomingPart->Release();
                pIncomingParts->Release();
                return hr;
            }
            pIncomingPart->Release();
        }

        pIncomingParts->Release();
    }

    return S_OK;
}

Sidetone::superMixFound 是一个布尔值成员,用于快速打破我们的递归循环并防止我们进一步遍历设备拓扑树(浪费时间)。

Sidetone::getPartName() 是一种简单的可重用方法,用于返回部件名称的宽字符串字符数组。

Sidetone::treePeek() returns 如果指定部分的 children 包含名称为第二个参数的部分,则为真。

Sidetone::getChildParts() returns 给定部分的每个 child 的指针数组。

弄清楚这一点后,只需将 setMute 方法暴露给 dllmain.cpp 并在我们需要 activate/deactivate 侧音时通过 JNA 调用它,所以在开头和任何传输结束。