Search code examples
c++comwasapi

Set WMP volume level


What i want to do is set the Windows Media Player's volume level. By default the volume is in-/decreased by 10% when e.g. clicking the down or up menu item (Play -> Volume -> Up), but this is, in my opinion, not fine enough (especially when e.g. skypeing with someone while listening to music).

The media player should stay an independent application.
Currently i'm using a little tool that sends app commands via SendMessage to the player with parameters as seen in spy++.

I thought of three ways to achieve my goal:

  • using WASAPI to get the media player's audio session and dynamically set the volume level
  • sending mouse down/up events to the volume slider of the media player host control by point
  • getting an instance of the media player control via IWMPPlayer4
  • including a media player control in my WPF application within a windows forms host (not preferred due to loss of independence)

Point 2 seems rather ugly due to the fact that the media player control is a COM element and has as far spy++ displays only one handle, meaning i would have to determine the exact position of the volume slider and send very precise mouse events. Additional i don't know whether this would work at all.

Point 3 has the presupposition that one can get an instance of a COM element by a handle. Since i have yet not worked with COM elements i don't know if this is possible.
Update: One can get an instance of a remote mediay player using the IWMPPlayer4 interface. Though i have to see whether one can change settings.

Point 1 has the impression on me that it would be possible without much effort. Though i'd be facing the next problem: identifying the media players audio session. Enumerating them using IAudioSessionManager2 and displaying the name using

IAudioSessionControl2 ctrl2 = NULL;
// ...
hr = ctrl2->GetDisplayName(&name);

if (FAILED(hr))
{
    SafeRelease(ctrl);
    SafeRelease(ctrl2);
    continue;
}

String ^sessionName = gcnew String(name);
Console::WriteLine("Session name: '" + sessionName + "'");

prints most the times an emtpy string except for Mozilla Firefox and System Sounds (the other processes might not have set a session name themselfes => a default name is chosen and GetDisplayName returns an empty string).

Update 2: As Simon Mourier pointed out one can compare the process ids to get the right ISimpleAudioVolume instance and it works as far as it comes to WMP to adopt the changes. The said instance is aquired the following way:

IMMDeviceEnumerator *pEnumerator = NULL;
ISimpleAudioVolume *pVolume = NULL;
IMMDevice *pDevice = NULL;
IAudioSessionManager2 *pManager = NULL;
IAudioSessionEnumerator *pSessionEnumerator = NULL;
int sessionCount = 0;

CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL,
    CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void**)&pEnumerator);
pEnumerator->GetDefaultAudioEndpoint(EDataFlow::eRender, ERole::eMultimedia, &pDevice);
pDevice->GetState(&deviceState);
pDevice->Activate(__uuidof(IAudioSessionManager2), CLSCTX_ALL, NULL, (void**)&pManager);
pManager->GetSessionEnumerator(&pSessionEnumerator);
pSessionEnumerator->GetCount(&sessionCount);

for (int i = 0; i < sessionCount; i++)
{
    IAudioSessionControl *ctrl = NULL;
    IAudioSessionControl2 *ctrl2 = NULL;
    DWORD processId = 0;

    hr = pSessionEnumerator->GetSession(i, &ctrl);

    if (FAILED(hr))
    {
        continue;
    }

    hr = ctrl->QueryInterface(__uuidof(IAudioSessionControl2), (void**)&ctrl2);

    if (FAILED(hr))
    {
        SafeRelease(ctrl);
        continue;
    }

    hr = ctrl2->GetProcessId(&processId);

    if (FAILED(hr))
    {
        SafeRelease(ctrl);
        SafeRelease(ctrl2);
        continue;
    }

    if (processId == wmpProcessId)
    {
        hr = ctrl2->QueryInterface(__uuidof(ISimpleAudioVolume), (void**)&pVolume);
        SafeRelease(ctrl);
        SafeRelease(ctrl2);
        break;
    }

    SafeRelease(ctrl);
    SafeRelease(ctrl2);
}

When aquiering an ISimpleAudioVolume instance via a IAudioClient one has to provide a session id to have volume changes reported to event subscribers. Is this possible using this approach?

Though i know that adding a media player control to my application would be the easiest way, i'd like to not use this option if possible.


Solution

  • I don't know what may happened during my initial try to set the media player's volume level, but the following code works (most exception handling excluded):

    HRESULT                 hr;
    IMMDeviceEnumerator     *pEnumerator = NULL;
    ISimpleAudioVolume      *pVolume = NULL;
    IMMDevice               *pDevice = NULL;
    IAudioSessionManager2   *pManager = NULL;
    IAudioSessionEnumerator *pSessionEnumerator = NULL;
    int                      sessionCount = 0;
    int                      wmpProcess = GetWmpProcessId(); // Aquire WMPs process id
    
    // Get the device enumerator and initialize the application for COM
    hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL,
             __uuidof(IMMDeviceEnumerator), (void**)&pEnumerator);
    
    // Get the default device
    hr = pEnumerator->GetDefaultAudioEndpoint(EDataFlow::eRender,
             ERole::eMultimedia, &pDevice);
    
    // Get the session 2 manager
    hr = pDevice->Activate(__uuidof(IAudioSessionManager2), CLSCTX_ALL,
             NULL, (void**)&pManager);
    
    // Get the session enumerator
    hr = pManager->GetSessionEnumerator(&pSessionEnumerator);
    
    // Get the session count
    hr = pSessionEnumerator->GetCount(&sessionCount);
    
    // Loop through all sessions
    for (int i = 0; i < sessionCount; i++)
    {
        IAudioSessionControl *ctrl = NULL;
        IAudioSessionControl2 *ctrl2 = NULL;
        DWORD processId = 0;
    
        hr = pSessionEnumerator->GetSession(i, &ctrl);
    
        if (FAILED(hr))
        {
            continue;
        }
    
        hr = ctrl->QueryInterface(__uuidof(IAudioSessionControl2), (void**)&ctrl2);
    
        if (FAILED(hr))
        {
            SafeRelease(ctrl);
            continue;
        }
    
        //Identify WMP process
        hr = ctrl2->GetProcessId(&processId);
    
        if (FAILED(hr))
        {
            SafeRelease(ctrl);
            SafeRelease(ctrl2);
            continue;
        }
    
        if (processId != wmpProcess)
        {
            SafeRelease(ctrl);
            SafeRelease(ctrl2);
            continue;
        }
    
        hr = ctrl2->QueryInterface(__uuidof(ISimpleAudioVolume), (void**)&pVolume);
    
        if (FAILED(hr))
        {
            Error(hr, "Failed to get ISimpleAudioVolume.");
    
            SafeRelease(ctrl);
            SafeRelease(ctrl2);
            continue;
        }
    
        // Set the master volume
        hr = pVolume->SetMasterVolume(1.0, NULL);
    
        if (FAILED(hr))
        {
            Error(hr, "Failed to set the master volume.");
            SafeRelease(ctrl);
            SafeRelease(ctrl2);
            SafeRelease(pVolume);
            continue;
        }
    
        SafeRelease(ctrl);
        SafeRelease(ctrl2);
        SafeRelease(pVolume);
    }