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.
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.