IMMNotificationClient and setDriverBug?

In our game (on windows ) we would like to handle audio driver output this way

  • either the player select “auto”, and we detect when the player changes default output in windows ( using IMMNotificationClient )
  • or the player is explicit about the output device, and we try to keep it, even if he changes the default output in Windows

For the first case, we are using this code ( [Windows] Possible bug with setDriver and getDriverInfo ) and everything is working
For the second case, we store the driver guid, and when an output change , we try to refind our driver and set it

Repro step in the sample provided sampling code:

  1. Launch sample ( with 3 audio output ) ( sound ok )
  2. Cycle from “DEFAULT” to the 2nd output ( sound ok )
  3. In Windows, change default output to the 3th output
  4. Note that the sound is now outputed to the default ( 0 ) driver, despite being setDriver to 2

Am I missing something ?

Thanks you

here is a sample code:

#include "fmod.hpp"
#include "common.h"
#include <initguid.h>
#include <mmdeviceapi.h>
#include <vector>

using namespace std;

const int   INTERFACE_UPDATETIME = 50;      // 50ms update for interface
const float DISTANCEFACTOR = 1.0f;          // Units per meter.  I.e feet would = 3.28.  centimeters would = 100.

class ENotificationClient : public IMMNotificationClient
{
public:
	HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId);

	HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR pwstrDeviceId)
	{
		UNREFERENCED_PARAMETER(pwstrDeviceId);
		return S_OK;
	}
	HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR pwstrDeviceId)
	{
		UNREFERENCED_PARAMETER(pwstrDeviceId);
		return S_OK;
	}
	HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState)
	{
		UNREFERENCED_PARAMETER(pwstrDeviceId);
		UNREFERENCED_PARAMETER(dwNewState);
		return S_OK;
	}
	HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR pwstrDeviceId, const PROPERTYKEY key)
	{
		UNREFERENCED_PARAMETER(pwstrDeviceId);
		UNREFERENCED_PARAMETER(key);
		return S_OK;
	}

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

	ULONG STDMETHODCALLTYPE AddRef()
	{
		return 1;
	}

	ULONG STDMETHODCALLTYPE Release()
	{
		return 0;
	}
};

#define EXIT_ON_ERROR(hres)  \
              if (FAILED(hres)) { goto Exit; }
#define SAFE_RELEASE(punk)  \
              if ((punk) != NULL)  \
                { (punk)->Release(); (punk) = NULL; }

const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
ENotificationClient NotificationClient;

CRITICAL_SECTION CriticalDevice;
bool DeviceAudioDefaultChanged = false;
FMOD_GUID DeviceAudioDefaultGUID = {};

HRESULT ENotificationClient::OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId)
{
	UNREFERENCED_PARAMETER(pwstrDeviceId);

	if (flow != EDataFlow::eRender ||
		role != ERole::eConsole)
	{
		return S_OK;
	}

	HRESULT hr = S_OK;
	IMMDeviceEnumerator *pEnumerator = NULL;
	IMMDevice *pEndpoint = NULL;
	IPropertyStore *pProps = NULL;
	LPWSTR pwszID = NULL;

	hr = CoCreateInstance(
		CLSID_MMDeviceEnumerator, NULL,
		CLSCTX_ALL, IID_IMMDeviceEnumerator,
		(void**)&pEnumerator);
	EXIT_ON_ERROR(hr);

	hr = pEnumerator->GetDefaultAudioEndpoint(flow, role, &pEndpoint);
	EXIT_ON_ERROR(hr);

	// Get the endpoint ID string.
	hr = pEndpoint->GetId(&pwszID);
	EXIT_ON_ERROR(hr);

	hr = pEndpoint->OpenPropertyStore(
		STGM_READ, &pProps);
	EXIT_ON_ERROR(hr);

	PROPVARIANT varGUID;
	// Initialize container for property value.
	PropVariantInit(&varGUID);

	hr = pProps->GetValue(
		PKEY_AudioEndpoint_GUID, &varGUID);
	EXIT_ON_ERROR(hr);

	CLSID deviceGuid;
	CLSIDFromString(varGUID.pwszVal, &deviceGuid);

	FMOD_GUID fmodDeviceID;
	fmodDeviceID.Data1 = deviceGuid.Data1;
	fmodDeviceID.Data2 = deviceGuid.Data2;
	fmodDeviceID.Data3 = deviceGuid.Data3;
	memcpy(fmodDeviceID.Data4, deviceGuid.Data4, sizeof(fmodDeviceID.Data4));

	EnterCriticalSection(&CriticalDevice);
	DeviceAudioDefaultChanged = true;
	memcpy(&DeviceAudioDefaultGUID, &fmodDeviceID, sizeof(DeviceAudioDefaultGUID));
	LeaveCriticalSection(&CriticalDevice);

	CoTaskMemFree(pwszID);
	pwszID = NULL;
	PropVariantClear(&varGUID);
	SAFE_RELEASE(pProps);
	SAFE_RELEASE(pEndpoint);

	SAFE_RELEASE(pEnumerator);

	return S_OK;

Exit:
	CoTaskMemFree(pwszID);
	SAFE_RELEASE(pEnumerator);
	SAFE_RELEASE(pEndpoint);
	SAFE_RELEASE(pProps);

	return S_OK;
}

int FMOD_Main()
{
    FMOD::System    *system;
    FMOD::Sound     *sound1, *sound2, *sound3;
    FMOD::Channel   *channel1 = 0, *channel2 = 0, *channel3 = 0;
    FMOD_RESULT      result;
    bool             listenerflag = true;
    FMOD_VECTOR      listenerpos  = { 0.0f, 0.0f, -1.0f * DISTANCEFACTOR };
    unsigned int     version;
    void            *extradriverdata = 0;

	InitializeCriticalSection(&CriticalDevice);

	if (FAILED(CoInitializeEx(nullptr, COINIT_MULTITHREADED)))
	{
		return 0;
	}

	IMMDeviceEnumerator *Enumerator = nullptr;
	HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER, __uuidof(IMMDeviceEnumerator), (void**)&Enumerator);
	if (SUCCEEDED(hr))
	{
		Enumerator->RegisterEndpointNotificationCallback(&NotificationClient);
	}

    Common_Init(&extradriverdata);

    /*
        Create a System object and initialize.
    */
    result = FMOD::System_Create(&system);
    ERRCHECK(result);
    
    result = system->getVersion(&version);
    ERRCHECK(result);

    if (version < FMOD_VERSION)
    {
        Common_Fatal("FMOD lib version %08x doesn't match header version %08x", version, FMOD_VERSION);
    }
    
    result = system->init(100, FMOD_INIT_NORMAL, extradriverdata);
    ERRCHECK(result);
    
    /*
        Set the distance units. (meters/feet etc).
    */
    result = system->set3DSettings(1.0, DISTANCEFACTOR, 1.0f);
    ERRCHECK(result);

    /*
        Load some sounds
    */
    result = system->createSound(Common_MediaPath("drumloop.wav"), FMOD_3D, 0, &sound1);
    ERRCHECK(result);
    result = sound1->set3DMinMaxDistance(0.5f * DISTANCEFACTOR, 5000.0f * DISTANCEFACTOR);
    ERRCHECK(result);
    result = sound1->setMode(FMOD_LOOP_NORMAL);
    ERRCHECK(result);

    result = system->createSound(Common_MediaPath("jaguar.wav"), FMOD_3D, 0, &sound2);
    ERRCHECK(result);
    result = sound2->set3DMinMaxDistance(0.5f * DISTANCEFACTOR, 5000.0f * DISTANCEFACTOR);
    ERRCHECK(result);
    result = sound2->setMode(FMOD_LOOP_NORMAL);
    ERRCHECK(result);

    result = system->createSound(Common_MediaPath("swish.wav"), FMOD_2D, 0, &sound3);
    ERRCHECK(result);

    /*
        Play sounds at certain positions
    */
    {
        FMOD_VECTOR pos = { -1.0f * DISTANCEFACTOR, 0.0f, 0.0f };
        FMOD_VECTOR vel = {  0.0f, 0.0f, 0.0f };

        result = system->playSound(sound1, 0, true, &channel1);
        ERRCHECK(result);
        result = channel1->set3DAttributes(&pos, &vel);
        ERRCHECK(result);
        result = channel1->setPaused(false);
        ERRCHECK(result);
    }

    {
        FMOD_VECTOR pos = { 1.0f * DISTANCEFACTOR, 0.0f, 0.0f };
        FMOD_VECTOR vel = { 0.0f, 0.0f, 0.0f };

        result = system->playSound(sound2, 0, true, &channel2);
        ERRCHECK(result);
        result = channel2->set3DAttributes(&pos, &vel);
        ERRCHECK(result);
        result = channel2->setPaused(false);
        ERRCHECK(result);
    }

    /*
        Main loop
    */
	int wantedDeviceIndex = -1;
	char wantedDeviceName[256];
	strcpy( wantedDeviceName, "DEFAULT" );
	FMOD_GUID wantedDeviceGUID = {};
	DeviceAudioDefaultChanged = true;

	struct DeviceCache
	{
		int id;
		char name[256];
		FMOD_GUID guid;
	};
	vector< DeviceCache > deviceCache;
    do
    {
		bool needToBuildDeviceCache = false;
		bool needToChangeDriver = false;
		EnterCriticalSection(&CriticalDevice);
		if( DeviceAudioDefaultChanged == true ) {
			printf( "DeviceAudioDefaultChanged" );
            FMOD_OUTPUTTYPE output;
            
            result = system->getOutput(&output);
            ERRCHECK(result);
            result = system->setOutput(FMOD_OUTPUTTYPE_NOSOUND);
            ERRCHECK(result);
            result = system->setOutput(output);
            ERRCHECK(result);

			needToBuildDeviceCache = true;

			DeviceAudioDefaultChanged = false;
		}

		LeaveCriticalSection( &CriticalDevice );

		if( needToBuildDeviceCache ) {
			deviceCache.clear();
			int numDrivers = 0;
			if( system->getNumDrivers( &numDrivers ) == FMOD_OK ) {
				for( int n = 0; n < numDrivers; ++n ) {
					char name[256] = {};
					FMOD_GUID guid = {};
					int systemrate = 0;
					FMOD_SPEAKERMODE speakermode = FMOD_SPEAKERMODE_DEFAULT;
					int speakermodechannels = 0;
					if( system->getDriverInfo( n, name, sizeof( name ), &guid, &systemrate, &speakermode, &speakermodechannels ) == FMOD_OK ) {
						DeviceCache dc;
						dc.id = n;
						dc.guid = guid;
						strcpy( dc.name, name );
						deviceCache.push_back( dc );
					}
				}
			}
			printf( "Device cache rebuild" );
			needToChangeDriver = true;
		}

		if( Common_BtnPress( BTN_ACTION1 ) )
		{
			wantedDeviceIndex++;
			if( wantedDeviceIndex >= deviceCache.size() )
				wantedDeviceIndex = -1;
			if( wantedDeviceIndex == -1 ) {
				//default 
				strcpy( wantedDeviceName, "DEFAULT" );
				memset( &wantedDeviceGUID, 0, sizeof( FMOD_GUID ) );
			} else {
				strcpy( wantedDeviceName, deviceCache[wantedDeviceIndex].name );
				memcpy( &wantedDeviceGUID, &deviceCache[wantedDeviceIndex].guid, sizeof( FMOD_GUID ) );
			}
			needToChangeDriver = true;
		}

		if( needToChangeDriver ) {
			DeviceCache* pDC = NULL;
			for( int i=0; i<deviceCache.size(); i++ ) {
				if( memcmp( &deviceCache[i].guid, &wantedDeviceGUID, sizeof(FMOD_GUID) ) == 0 ) {
					pDC = &deviceCache[i];
				}
			}
			system->setDriver( pDC==NULL ? 0 : pDC->id );
			needToChangeDriver = false;
		}

		
        Common_Update();
        result = system->update();
        ERRCHECK(result);
        Common_Draw("==================================================");
		int currentDriver;
		if( system->getDriver( &currentDriver) == FMOD_OK ) {
			char name[256] = {};
			FMOD_GUID guid = {};
			if( system->getDriverInfo( currentDriver, name, sizeof( name ), &guid, NULL, NULL, NULL ) == FMOD_OK ) {
				Common_Draw( "Current Driver (%d) %s", currentDriver, name );
			}
		}
		

		Common_Draw( "Wanted Driver (%d) %s", wantedDeviceIndex, wantedDeviceName );
		Common_Draw("Press %s to cycle wanted driver", Common_BtnStr(BTN_ACTION1));
        Common_Draw("");

        Common_Sleep(INTERFACE_UPDATETIME - 1);
    } while (!Common_BtnPress(BTN_QUIT));

    /*
        Shut down
    */
    result = sound1->release();
    ERRCHECK(result);
    result = sound2->release();
    ERRCHECK(result);
    result = sound3->release();
    ERRCHECK(result);

    result = system->close();
    ERRCHECK(result);
    result = system->release();
    ERRCHECK(result);

    Common_Close();

	if (Enumerator)
	{
		Enumerator->UnregisterEndpointNotificationCallback(&NotificationClient);
		Enumerator->Release();
		Enumerator = nullptr;
	}

	CoUninitialize();

    return 0;
}

Thanks very much for all the details, this does appear to be a bug.

You can currently work around this by calling:

result = system->setDriver(0);

Just after the second setOutput, there is an internally stored GUID that does not get changed when setting the output as you have. By calling setDriver it will force the GUID to be updated.

Hi,
thanks cameron, the workaround works.
Do you plan to fix it in a future release ?
Regards