Device Events (Core Audio APIs)

A device event notifies clients of a change in the status of an audio endpoint device in the system. The following are examples of device events:

  • The user enables or disables an audio endpoint device from Device Manager or from the Windows multimedia control panel, Mmsys.cpl.
  • The user adds an audio adapter to the system or removes an audio adapter from the system.
  • The user plugs an audio endpoint device into an audio jack with jack-presence detection, or removes an audio endpoint device from such a jack.
  • The user changes the device role that is assigned to a device.
  • The value of a property of a device changes.

The addition or removal of an audio adapter generates device events for all of the audio endpoint devices that connect to the adapter. The first four items in the preceding list are examples of device state changes. For more information about the device states of audio endpoint devices, see DEVICE_STATE_XXX Constants. For more information about jack-presence detection, see Audio Endpoint Devices.

A client can register to be notified when device events occur. In response to these notifications, the client can dynamically change the way that it uses a particular device, or select a different device to use for a particular purpose.

For example, if an application is playing an audio track through a set of USB speakers, and the user disconnects the speakers from the USB connector, the application receives a device-event notification. In response to the event, if the application detects that a set of desktop speakers is connected to the integrated audio adapter on the system motherboard, the application can resume playing the audio track through the desktop speakers. In this example, the transition from USB speakers to desktop speakers occurs automatically, without requiring the user to intervene by explicitly redirecting the application.

To register to receive device notifications, a client calls the IMMDeviceEnumerator::RegisterEndpointNotificationCallback method. When the client no longer requires notifications, it cancels them by calling the IMMDeviceEnumerator::UnregisterEndpointNotificationCallback method. Both methods take an input parameter, named pClient, that is a pointer to an IMMNotificationClient interface instance.

The IMMNotificationClient interface is implemented by a client. The interface contains several methods, each of which serves as a callback routine for a particular type of device event. When a device event occurs in an audio endpoint device, the MMDevice module calls the appropriate method in the IMMNotificationClient interface of every client that is currently registered to receive device-event notifications. These calls pass a description of the event to the clients. For more information, see IMMNotificationClient Interface.

A client that is registered to receive device-event notifications will receive notifications of all types of device events that occur in all of the audio endpoint devices in the system. If a client is interested only in certain event types or in certain devices, then the methods in its IMMNotificationClient implementation should filter the events appropriately.

The Windows SDK provides samples that include several implementations for the IMMNotificationClient Interface. For more information, see SDK Samples That Use the Core Audio APIs.

The following code example shows a possible implementation of the IMMNotificationClient interface:

//-----------------------------------------------------------
// Example implementation of IMMNotificationClient interface.
// When the status of audio endpoint devices change, the
// MMDevice module calls these methods to notify the client.
//-----------------------------------------------------------

#define SAFE_RELEASE(punk)  \
              if ((punk) != NULL)  \
                { (punk)->Release(); (punk) = NULL; }

class CMMNotificationClient : public IMMNotificationClient
{
    LONG _cRef;
    IMMDeviceEnumerator *_pEnumerator;

    // Private function to print device-friendly name
    HRESULT _PrintDeviceName(LPCWSTR  pwstrId);

public:
    CMMNotificationClient() :
        _cRef(1),
        _pEnumerator(NULL)
    {
    }

    ~CMMNotificationClient()
    {
        SAFE_RELEASE(_pEnumerator)
    }

    // IUnknown methods -- AddRef, Release, and QueryInterface

    ULONG STDMETHODCALLTYPE AddRef()
    {
        return InterlockedIncrement(&_cRef);
    }

    ULONG STDMETHODCALLTYPE Release()
    {
        ULONG ulRef = InterlockedDecrement(&_cRef);
        if (0 == ulRef)
        {
            delete this;
        }
        return ulRef;
    }

    HRESULT STDMETHODCALLTYPE QueryInterface(
                                REFIID riid, VOID **ppvInterface)
    {
        if (IID_IUnknown == riid)
        {
            AddRef();
            *ppvInterface = (IUnknown*)this;
        }
        else if (__uuidof(IMMNotificationClient) == riid)
        {
            AddRef();
            *ppvInterface = (IMMNotificationClient*)this;
        }
        else
        {
            *ppvInterface = NULL;
            return E_NOINTERFACE;
        }
        return S_OK;
    }

    // Callback methods for device-event notifications.

    HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(
                                EDataFlow flow, ERole role,
                                LPCWSTR pwstrDeviceId)
    {
        char  *pszFlow = "?????";
        char  *pszRole = "?????";

        _PrintDeviceName(pwstrDeviceId);

        switch (flow)
        {
        case eRender:
            pszFlow = "eRender";
            break;
        case eCapture:
            pszFlow = "eCapture";
            break;
        }

        switch (role)
        {
        case eConsole:
            pszRole = "eConsole";
            break;
        case eMultimedia:
            pszRole = "eMultimedia";
            break;
        case eCommunications:
            pszRole = "eCommunications";
            break;
        }

        printf("  -->New default device: flow = %s, role = %s\n",
               pszFlow, pszRole);
        return S_OK;
    }

    HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR pwstrDeviceId)
    {
        _PrintDeviceName(pwstrDeviceId);

        printf("  -->Added device\n");
        return S_OK;
    };

    HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR pwstrDeviceId)
    {
        _PrintDeviceName(pwstrDeviceId);

        printf("  -->Removed device\n");
        return S_OK;
    }

    HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(
                                LPCWSTR pwstrDeviceId,
                                DWORD dwNewState)
    {
        char  *pszState = "?????";

        _PrintDeviceName(pwstrDeviceId);

        switch (dwNewState)
        {
        case DEVICE_STATE_ACTIVE:
            pszState = "ACTIVE";
            break;
        case DEVICE_STATE_DISABLED:
            pszState = "DISABLED";
            break;
        case DEVICE_STATE_NOTPRESENT:
            pszState = "NOTPRESENT";
            break;
        case DEVICE_STATE_UNPLUGGED:
            pszState = "UNPLUGGED";
            break;
        }

        printf("  -->New device state is DEVICE_STATE_%s (0x%8.8x)\n",
               pszState, dwNewState);

        return S_OK;
    }

    HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(
                                LPCWSTR pwstrDeviceId,
                                const PROPERTYKEY key)
    {
        _PrintDeviceName(pwstrDeviceId);

        printf("  -->Changed device property "
               "{%8.8x-%4.4x-%4.4x-%2.2x%2.2x-%2.2x%2.2x%2.2x%2.2x%2.2x%2.2x}#%d\n",
               key.fmtid.Data1, key.fmtid.Data2, key.fmtid.Data3,
               key.fmtid.Data4[0], key.fmtid.Data4[1],
               key.fmtid.Data4[2], key.fmtid.Data4[3],
               key.fmtid.Data4[4], key.fmtid.Data4[5],
               key.fmtid.Data4[6], key.fmtid.Data4[7],
               key.pid);
        return S_OK;
    }
};

// Given an endpoint ID string, print the friendly device name.
HRESULT CMMNotificationClient::_PrintDeviceName(LPCWSTR pwstrId)
{
    HRESULT hr = S_OK;
    IMMDevice *pDevice = NULL;
    IPropertyStore *pProps = NULL;
    PROPVARIANT varString;

    CoInitialize(NULL);
    PropVariantInit(&varString);

    if (_pEnumerator == NULL)
    {
        // Get enumerator for audio endpoint devices.
        hr = CoCreateInstance(__uuidof(MMDeviceEnumerator),
                              NULL, CLSCTX_INPROC_SERVER,
                              __uuidof(IMMDeviceEnumerator),
                              (void**)&_pEnumerator);
    }
    if (hr == S_OK)
    {
        hr = _pEnumerator->GetDevice(pwstrId, &pDevice);
    }
    if (hr == S_OK)
    {
        hr = pDevice->OpenPropertyStore(STGM_READ, &pProps);
    }
    if (hr == S_OK)
    {
        // Get the endpoint device's friendly-name property.
        hr = pProps->GetValue(PKEY_Device_FriendlyName, &varString);
    }
    printf("----------------------\nDevice name: \"%S\"\n"
           "  Endpoint ID string: \"%S\"\n",
           (hr == S_OK) ? varString.pwszVal : L"null device",
           (pwstrId != NULL) ? pwstrId : L"null ID");

    PropVariantClear(&varString);

    SAFE_RELEASE(pProps)
    SAFE_RELEASE(pDevice)
    CoUninitialize();
    return hr;
}

The CMMNotificationClient class in the preceding code example is an implementation of the IMMNotificationClient interface. Because IMMNotificationClient inherits from IUnknown, the class definition contains implementations of the IUnknown methods AddRef, Release, and QueryInterface. The remaining public methods in the class definition are specific to the IMMNotificationClient interface. These methods are:

  • OnDefaultDeviceChanged, which is called when the user changes the device role of an audio endpoint device.
  • OnDeviceAdded, which is called when the user adds an audio endpoint device to the system.
  • OnDeviceRemoved, which is called when the user removes an audio endpoint device from the system.
  • OnDeviceStateChanged, which is called when the device state of an audio endpoint device changes. (For more information about device states, see DEVICE_STATE_ XXX Constants.)
  • OnPropertyValueChanged, which is called when the value of a property of an audio endpoint device changes.

Each of these methods takes an input parameter, pwstrDeviceId, that is a pointer to an endpoint ID string. The string identifies the audio endpoint device in which the device event occurred.

In the preceding code example, _PrintDeviceName is a private method in the CMMNotificationClient class that prints the friendly name of the device. _PrintDeviceName takes the endpoint ID string as an input parameter. It passes the string to the IMMDeviceEnumerator::GetDevice method. GetDevice creates an endpoint device object to represent the device and provides the IMMDevice interface to that object. Next, _PrintDeviceName calls the IMMDevice::OpenPropertyStore method to retrieve the IPropertyStore interface to the device's property store. Finally, _PrintDeviceName calls the IPropertyStore::GetItem method to obtain the friendly-name property of the device. For more information about IPropertyStore, see the Windows SDK documentation.

In addition to device events, clients can register to receive notifications of audio-session events and endpoint-volume events. For more information, see IAudioSessionEvents Interface and IAudioEndpointVolumeCallback Interface.

Audio Endpoint Devices