Search code examples
c++windowsaudioopenal

How to detect default audio output device change in Windows


I am developing an audio recording program in C++ (I use OpenAL and ImGui with OpenGL if that helps) and i want to know if i can detect if my default audio output device changes without running a loop that blocks my program. Is there a way for me to detect it like with callbacks maybe?

I tried using

alcGetString(device, ALC_DEFAULT_DEVICE_SPECIFIER);

function to get the name of the default device and compare it with last one i used in a loop on another thread. It did the job but it cost me lots of performance.


Solution

  • Thanks to @PaulSanders i have found a solution. It is not what i was looking for but i think it is still a good one. Here is the code:

    #include <Windows.h>
    #include <mmdeviceapi.h>
    #include <endpointvolume.h>
    
    // The notification client class
    class NotificationClient : public IMMNotificationClient {
    public:
        // IUnknown methods
        HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) {
            if (riid == IID_IUnknown || riid == __uuidof(IMMNotificationClient)) {
                *ppvObject = this;
                AddRef();
                return S_OK;
            }
            *ppvObject = NULL;
            return E_NOINTERFACE;
        }
        ULONG STDMETHODCALLTYPE AddRef() { return InterlockedIncrement(&m_cRef); }
        ULONG STDMETHODCALLTYPE Release() {
            ULONG ulRef = InterlockedDecrement(&m_cRef);
            if (ulRef == 0)
                delete this;
            return ulRef;
        }
    
        // IMMNotificationClient methods
        HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) {
            // The default audio output device has changed
            // Handle the device change event
            // ...
            return S_OK;
        }
        HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR pwstrDeviceId) { return S_OK; }
        HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR pwstrDeviceId) { return S_OK; }
        HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState) { return S_OK; }
        HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR pwstrDeviceId, const PROPERTYKEY key) { return S_OK; }
    
        // Constructor and destructor
        NotificationClient() : m_cRef(1) {}
        ~NotificationClient() {}
    
    private:
        long m_cRef;
    };
    
    class AudioDeviceNotificationListener
    {
    public:
        AudioDeviceNotificationListener() = default;
        ~AudioDeviceNotificationListener() { Close(); }
    
        bool Start()
        {
            if (!bDidStart)
            {
                // Initialize the COM library for the current thread
                HRESULT hr = CoInitialize(NULL);
                if (FAILED(hr)) {
                    return false;
                }
    
                // Create the device enumerator
                hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void**)&pEnumerator);
                if (FAILED(hr)) {
                    CoUninitialize();
                    return false;
                }
    
                // Create the notification client object
                pNotificationClient = new NotificationClient();
    
                // Register the notification client
                hr = pEnumerator->RegisterEndpointNotificationCallback(pNotificationClient);
                if (FAILED(hr)) {
                    pEnumerator->Release();
    
                    pNotificationClient->Release();
                    pNotificationClient = nullptr;
    
                    CoUninitialize();
                    return false;
                }
    
                // Create the notification thread
                hNotificationThread = CreateThread(NULL, 0, &AudioDeviceNotificationListener::NotificationThreadProc, pNotificationClient, 0, NULL);
                if (hNotificationThread == NULL) {
                    pEnumerator->UnregisterEndpointNotificationCallback(pNotificationClient);
                    pEnumerator->Release();
    
                    pNotificationClient->Release();
                    pNotificationClient = nullptr;
    
                    CoUninitialize();
                    return false;
                }
    
                bDidStart = true;
                return true;
            }
    
            return false;
        }
    
        void Close()
        {
            if (bDidStart)
            {
                // Clean up
    
                CloseThread();
    
                pEnumerator->UnregisterEndpointNotificationCallback(pNotificationClient);
                pEnumerator->Release();
    
                pNotificationClient->Release();
                pNotificationClient = nullptr;
    
                CoUninitialize();
                bDidStart = false;
            }
        }
    
    private:
        void CloseThread()
        {
            PostThreadMessage(GetThreadId(hNotificationThread), WM_QUIT, NULL, NULL);
            WaitForSingleObject(hNotificationThread, INFINITE);
        }
    
        // Thread Function
        static DWORD WINAPI NotificationThreadProc(LPVOID lpParameter)
        {
            // Run the message loop
            MSG msg;
            while (true) {
                // Check for a message
                if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
                    // A message was received. Process it.
                    TranslateMessage(&msg);
    
                    // If WM_QUIT message is received quit the thread
                    if (msg.message == WM_QUIT) {
                        break;
                    }
    
                    DispatchMessage(&msg);
                }
                else {
                    // No message was received. Suspend the thread until a message is received.
                    WaitMessage();
                }
            }
    
            return 0;
        }
    
    private:
        bool bDidStart = false;
    
        NotificationClient* pNotificationClient = nullptr;
    
        IMMDeviceEnumerator* pEnumerator = NULL;
        HANDLE hNotificationThread = NULL;
    };
    

    Let me explain the code: NotificationClient class inherits IMMNotificationClient so i can override its functions like OnDefaultDeviceChanged to handle audio output device change for my app. You can also add your own logic to functions like OnDeviceAdded or OnDeviceRemoved to handle other types of events, but since i don't need them i just return s_Ok from those functions. You should also know that those functions are pure-virtual functions so you need to override them even if you don't want to use them. I use IMMDeviceEnumerator so i can register my inherited NotificationClient class to receive audio device messages. But if the COM library isn't initialized then you need to call CoInitialize function to initialize it. I create a thread with a loop and use PeekMessage to get messages and use WaitMessage function to suspend the thread until it receives another message. This solves my performance problem with busy-looping to check for a message continually. To close this thread safely i send a WM_QUIT message to the thread using PostThreadMessage function and use WaitForSingleObject to wait for it to close.

    I wrapped all of this to a AudioDeviceNotificationListener class so i can just call Start function to begin listening for messages and Close function to exit the thread and stop listening.

    (Edit: I also found a way without creating a thread. The code is pretty much the same, i just removed AudioDeviceNotificationListener class. The code is shown below)

    // The notification client class
        class NotificationClient : public IMMNotificationClient {
        public:
            NotificationClient() {
                Start();
            }
    
            ~NotificationClient() {
                Close();
            }
    
            bool Start() {
                // Initialize the COM library for the current thread
                HRESULT ihr = CoInitialize(NULL);
    
                if (SUCCEEDED(ihr)) {
                    // Create the device enumerator
                    IMMDeviceEnumerator* pEnumerator;
                    HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void**)&pEnumerator);
                    if (SUCCEEDED(hr)) {
                        // Register for device change notifications
                        hr = pEnumerator->RegisterEndpointNotificationCallback(this);
                        m_pEnumerator = pEnumerator;
    
                        return true;
                    }
    
                    CoUninitialize();
                }
    
                return false;
            }
    
            void Close() {
                // Unregister the device enumerator
                if (m_pEnumerator) {
                    m_pEnumerator->UnregisterEndpointNotificationCallback(this);
                    m_pEnumerator->Release();
                }
    
                // Uninitialize the COM library for the current thread
                CoUninitialize();
            }
    
            // IUnknown methods
            STDMETHOD(QueryInterface)(REFIID riid, void** ppvObject) {
                if (riid == IID_IUnknown || riid == __uuidof(IMMNotificationClient)) {
                    *ppvObject = static_cast<IMMNotificationClient*>(this);
                    AddRef();
                    return S_OK;
                }
                return E_NOINTERFACE;
            }
    
            ULONG STDMETHODCALLTYPE AddRef() {
                return InterlockedIncrement(&m_cRef);
            }
    
            ULONG STDMETHODCALLTYPE Release() {
                ULONG ulRef = InterlockedDecrement(&m_cRef);
                if (0 == ulRef) {
                    delete this;
                }
                return ulRef;
            }
    
            // IMMNotificationClient methods
            STDMETHOD(OnDefaultDeviceChanged)(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) {
                // Default audio device has been changed.
                
                return S_OK;
            }
    
            STDMETHOD(OnDeviceAdded)(LPCWSTR pwstrDeviceId) {
                // A new audio device has been added.
                return S_OK;
            }
    
            STDMETHOD(OnDeviceRemoved)(LPCWSTR pwstrDeviceId) {
                // An audio device has been removed.
                return S_OK;
            }
    
            STDMETHOD(OnDeviceStateChanged)(LPCWSTR pwstrDeviceId, DWORD dwNewState) {
                // The state of an audio device has changed.
                return S_OK;
            }
    
            STDMETHOD(OnPropertyValueChanged)(LPCWSTR pwstrDeviceId, const PROPERTYKEY key) {
                // A property value of an audio device has changed.
                return S_OK;
            }
    
        private:
            LONG m_cRef;
            IMMDeviceEnumerator* m_pEnumerator;
        };
    

    You can use one of these codes for your own preference both of them works for me.