Search code examples
javac++windowsaudiocom

How to enable sidetone/microphone pass-thru programmatically


For my current project I'm implementing a native library in C++ that I'll be accessing via JNA, this project is a low-latency communication simulator. There's a requirement to enable sidetone while transmitting in order to mimic the hardware the simulator is based on. Of course JAVA sound is proving difficult to achieve near-zero latency (best we can get is ~120ms), in order to remain comprehensible we need the latency on sidetone to be near-zero. Fortunately it seems that in Windows there's a method to listen to the usb headset's microphone which produces perfect sidetone.

Audio Properties -> Playback -> Headset Earphone -> Properties -> Levels

An example of what I mean here

(Note that this is different from the 'listen to this device' feature which produces a pretty bad delay)

I've been working with the MSDN examples for the Core Audio API's and am able to query devices and get their channels, volume levels, mute setting, etc. but the microphone level mute/unmute doesn't seem to be accessible from even the core audio apis.

My question is this: is there a way to programmatically interface with a usb headset's microphone level/mute setting?

Our simulators are standardized so we don't have to worry about supporting a wide range of headsets (2 at the moment).


Solution

  • The key to solving this problem was to walk the device topology tree backwards until I found the part responsible for setting the sidetone mute attribute. So in my CPP project I had several methods working together to determine where I was in the topology tree looking for a SuperMix part.

    SuperMix seems to be a common name for sidetone and is at least used by the two headsets we support. The tree is identical for both headsets, your mileage may vary. This is what the output may look like from the aforementioned WalkTreeBackwardsFromPart example (see this answer)

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

    Here's my modified version of WalkTreeBackwardsFromPart, which for all intents and purposes simply checks whether or not the part we're currently looking at is the SuperMix and the direct child of this part is a volume node, this is to prevent an incorrect assignment as I found that for our headsets there would often be two nodes called SuperMix and the only difference was that the one we wanted had a volume node 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 is a boolean member used to quickly break our recursive loop and prevent us from walking the device topology tree any further (wasted time).

    Sidetone::getPartName() is a simple reusable method for returning a widestring character array of the part's name.

    Sidetone::treePeek() returns true if the children of the specified part contains a part with the name specified as the second parameter.

    Sidetone::getChildParts() returns an array of pointers for each child of a given part.

    After figuring this out it was just a matter of exposing the setMute method to dllmain.cpp and calling it via JNA whenever we needed to activate/deactivate sidetone, so at the beginning and ending of any transmission.