-2

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.

  • Is performance really a problem with detecting such device, and then bind your callback to a specific one detected? Are you trying to detect it with each and every callback while recording? Your question us unclear, and needs more details about the code you're actually running. – πάντα ῥεῖ Dec 30 '22 at 19:46
  • @πάνταῥεῖ Oh sorry, i thought i was clear, let me answer your questions. Yes, performance is really a problem because when i use a loop to check for a device change, my computer turns into a ww2 aircraft so yeah i think you should get my point. No i am not using any callbacks, i clarified in my question that i get the default device name in a loop and compare it with the last one to check if it has changed. – ThewyRogue99 Dec 30 '22 at 19:53
  • 1
    _when i use a loop to check for a device change, my computer turns into a ww2 aircraft_ Are you busy-looping? – Paul Sanders Dec 30 '22 at 20:22
  • Take a look at [IMMDeviceEnumerator::RegisterEndpointNotificationCallback](https://learn.microsoft.com/en-us/windows/win32/api/mmdeviceapi/nf-mmdeviceapi-immdeviceenumerator-registerendpointnotificationcallback). What you want is in there somewhere. – Paul Sanders Dec 30 '22 at 20:26
  • @PaulSanders i took a look at IMMDeviceEnumerator::RegisterEndpointNotificationCallback and found a solution. It's not the one i was looking for actually but it is pretty good for me thanks for your help. I will add my solution here. – ThewyRogue99 Dec 30 '22 at 22:34
  • Please provide enough code so others can better understand or reproduce the problem. – Community Dec 30 '22 at 23:55

1 Answers1

2

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.

  • Don't (ever!) use `TerminateThread`, it is deeply flawed. Instead, set a flag which the thread itself can check and then exit cleanly. You can wake the thread up after setting the flag by posting (say) a WM_NULL message to the thread and then wait for it to exit using `WaitFortSingleObject` on the thread handle before moving on. – Paul Sanders Dec 31 '22 at 00:20
  • @PaulSanders thank you, i didn't know that i could send a message to wake the thread up i will edit my answer so that i can stop the thread without calling TerminateThread. – ThewyRogue99 Dec 31 '22 at 08:13
  • 1
    @PaulSanders i also found a way to run this code without creating a thread. I edited my answer, you should check it out. – ThewyRogue99 Dec 31 '22 at 12:36
  • Yes, that makes more sense. I did wonder why you were launching that extra thread. – Paul Sanders Dec 31 '22 at 12:54
  • @PaulSanders Well i also was wondering why i was launching that thread. I actually found that code on internet. My theory is that these notfication messages are processed by an HWND window. Since my app has a window, this code can run without a separate thread. But if you have a console app, i think that thread will be necesarry. – ThewyRogue99 Dec 31 '22 at 16:44