Fun with the endpoint volume interfaces - closing the loop

Yesterday, I added support for metering to the cheesy OSD application, today I want to add in the one thing that's been missing: notification of external volume changes.

The good news is that once again, it's pretty easy to add support.  All you need to do is to define a class to handle the notification:

 class CVolumeNotification : public IAudioEndpointVolumeCallback 
{ 
    LONG m_RefCount; 
    ~CVolumeNotification(void) {}; 
public: 
    CVolumeNotification(void) : m_RefCount(1) 
    { 
    } 
    STDMETHODIMP_(ULONG)AddRef() { return InterlockedIncrement(&m_RefCount); } 
    STDMETHODIMP_(ULONG)Release()  
    { 
        LONG ref = InterlockedDecrement(&m_RefCount);  
        if (ref == 0) 
            delete this; 
        return ref; 
    } 
    STDMETHODIMP QueryInterface(REFIID IID, void **ReturnValue) 
    { 
        if (IID == IID_IUnknown || IID== __uuidof(IAudioEndpointVolumeCallback))  
        { 
            *ReturnValue = static_cast<IUnknown*>(this); 
            AddRef(); 
            return S_OK; 
        } 
        *ReturnValue = NULL; 
        return E_NOINTERFACE; 
    } 

    STDMETHODIMP OnNotify(PAUDIO_VOLUME_NOTIFICATION_DATA NotificationData) 
    { 
        wchar_t outputString[256]; 
        DWORD written; 
        COORD writeCoord; 
        StringCbPrintf(outputString, sizeof(outputString), L"Volume Changed: %f", NotificationData->fMasterVolume); 

        writeCoord.X = 0; 
        writeCoord.Y = 3; 
        WriteConsoleOutputCharacter(GetStdHandle(STD_OUTPUT_HANDLE), outputString, (DWORD)wcslen(outputString), writeCoord, &written); 
        return S_OK; 
    } 
}; 

This function is mostly COM goo to handle references, the guts of the function simply print out the new master volume.  If I was writing a real OSD, I'd have the volume callback object keep a reference to the endpoint volume interface and update the OSD with the current step information, but for a cheesy application like this one, it'll do.  Please note that it writes the notification to line 3 - that's to handle the case where the application runs in a window that is narrower than 100 characters - in that case, the meter wraps to line 2 :).

Of course that's not all you need to do - you need to instantiate a volume notification object and register it for notifications (I've included some lines from the previous versions for context).

     hr = defaultDevice->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_INPROC_SERVER, NULL, (LPVOID *)&endpointVolume); 

    CVolumeNotification *volumeNotification = new CVolumeNotification(); 

    hr = endpointVolume->RegisterControlChangeNotify(volumeNotification); 

    hr = defaultDevice->Activate(__uuidof(IAudioMeterInformation), CLSCTX_INPROC_SERVER, NULL, (LPVOID *)&context._Meter); 

and finally, to clean things up (again, I've included some lines from yesterday for context):

     context._Meter->Release(); 
    // 
    //    Remove our notification. 
    // 
    endpointVolume->UnregisterControlChangeNotify(volumeNotification); 

    endpointVolume->Release(); 
    volumeNotification->Release(); 
    SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), oldMode); 

There's still one minor problem with this sample, but it's a big one that will absolutely hit people who try to use this functionality in a real application.  I'll talk about that and show how to fix it next.

And again, this is a poor sample - there is no attempt at useful things like error recovery and the like.  It's just to show how this stuff works.