Click here to Skip to main content
15,885,686 members
Articles / Desktop Programming / MFC
Article

Volume Controller

Rate me:
Please Sign up or sign in to vote.
2.40/5 (15 votes)
23 Dec 2007CPOL6 min read 68.1K   2.5K   18   8
This application can control the system volume using the mouse buttons.
Sample Image - Volume_Control.jpg

Introduction

This is a simple application to control the system volume. Mainly two sets of events are handled in this application; the mouse middle button events and the windows session change events.

Handling the first set of events, the scroll wheel can be used to increase/decrease the volume and the middle mouse button can set the mute to ON/OFF. The application is also hooked to the Windows default volume control. That is, whenever the system volume level changes, application™'s volume level is updated.

And for the second set of events, the windows session Lock/Unlock events are handled. The music player is paused and system volume is mute for SESSION LOCK and the status is reverted for SESSION UNLOCK. The following music players are currently supported:

  • Windows Media Player
  • Jet Audio
  • Winamp
  • Apple iTunes

A mouse hook has been implemented to get the mouse actions. The source code and the binaries of the mouse hook are also included in the attached zip files. The mouse hook will only be explained in brief since it has been covered in detail by other authors.

The binaries for Volume Control and the Mouse Hook should be placed in the same folder. The application only has a system tray icon. The Left/Right click on the system tray icon will display a context menu. Currently, the context menu has only a Close button. Other buttons can be added as per your requirement.

The white horizontal line on the system tray icon shows the current system volume level. To control the system volume, press CTRL + SHIFT and scroll the mouse wheel. CTRL + SHIFT + the middle mouse button will toggle mute ON/OFF.

Volume Control - Under the Hood

This section was named as per Microsoft conventions. The various code portions of the mixer control were inspired from other articles on CodeProject itself. Ample changes were made to the code portions to make it seem that every bit came out from my head.

Two types of controlling are done through this application, firstly it controls the system volume level and secondly, this application controls the playback of various music players. I'll start with system volume control.

System Volume Control

There is nothing complicated about the code. The only challenge that I had to face was to redraw the icon to correspond to the current volume level. From my point of view, I could not come out with a better idea than drawing the icon at runtime.

I used the CreateIcon API. The important and only problem with this method was the number of colors that could be used. The number of colors was actually limited to four: black, white, the background color, and the reverse of the background color. For my icon, I only used black and white.

Two 32x32 byte arrays were used to generate the icon, an AND array, and a XOR array. The entire icon was initialized as black, and the position of the volume level bar was calculated using a very basic mathematical logic and that portion of the icon was set as white.

C++
// Icon AND bitmask
BYTE ANDmaskIcon[] = {
0xF0, 0x00, 0x00, 0x0F,   // line 1
0xF0, 0x00, 0x00, 0x0F,   // line 2
0xF0, 0x00, 0x00, 0x0F,   // line 3
0xF0, 0x00, 0x00, 0x0F,   // line 4

0xF0, 0x00, 0x00, 0x0F,   // line 5
0xF0, 0x00, 0x00, 0x0F,   // line 6
0xF0, 0x00, 0x00, 0x0F,   // line 7
0xF0, 0x00, 0x00, 0x0F,   // line 8

0xF0, 0x00, 0x00, 0x0F,   // line 9
0xF0, 0x00, 0x00, 0x0F,   // line 10
0xF0, 0x00, 0x00, 0x0F,   // line 11
0xF0, 0x00, 0x00, 0x0F,   // line 12

0xF0, 0x00, 0x00, 0x0F,   // line 13
0xF0, 0x00, 0x00, 0x0F,   // line 14
0xF0, 0x00, 0x00, 0x0F,   // line 15
0xF0, 0x00, 0x00, 0x0F,   // line 16

0xF0, 0x00, 0x00, 0x0F,   // line 17
0xF0, 0x00, 0x00, 0x0F,   // line 18
0xF0, 0x00, 0x00, 0x0F,   // line 19
0xF0, 0x00, 0x00, 0x0F,   // line 20

0xF0, 0x00, 0x00, 0x0F,   // line 21
0xF0, 0x00, 0x00, 0x0F,   // line 22
0xF0, 0x00, 0x00, 0x0F,   // line 23
0xF0, 0x00, 0x00, 0x0F,   // line 24

0xF0, 0x00, 0x00, 0x0F,   // line 25
0xF0, 0x00, 0x00, 0x0F,   // line 26
0xF0, 0x00, 0x00, 0x0F,   // line 27
0xF0, 0x00, 0x00, 0x0F,   // line 28

0xF0, 0x00, 0x00, 0x0F,   // line 29
0xF0, 0x00, 0x00, 0x0F,   // line 30
0xF0, 0x00, 0x00, 0x0F,   // line 31
0xF0, 0x00, 0x00, 0x0F};  // line 32

// Icon XOR bitmask
BYTE XORmaskIcon[] = {
0x00, 0x00, 0x00, 0x00,   // line 1
0x00, 0x00, 0x00, 0x00,   // line 2
0x00, 0x00, 0x00, 0x00,   // line 3
0x00, 0x00, 0x00, 0x00,   // line 4

0x00, 0x00, 0x00, 0x00,   // line 5
0x00, 0x00, 0x00, 0x00,   // line 6
0x00, 0x00, 0x00, 0x00,   // line 7
0x00, 0x00, 0x00, 0x00,   // line 8

0x00, 0x00, 0x00, 0x00,   // line 9
0x00, 0x00, 0x00, 0x00,   // line 10
0x00, 0x00, 0x00, 0x00,   // line 11
0x00, 0x00, 0x00, 0x00,   // line 12

0x00, 0x00, 0x00, 0x00,   // line 13
0x00, 0x00, 0x00, 0x00,   // line 14
0x00, 0x00, 0x00, 0x00,   // line 15
0x00, 0x00, 0x00, 0x00,   // line 16

0x00, 0x00, 0x00, 0x00,   // line 17
0x00, 0x00, 0x00, 0x00,   // line 18
0x00, 0x00, 0x00, 0x00,   // line 19
0x00, 0x00, 0x00, 0x00,   // line 20

0x00, 0x00, 0x00, 0x00,   // line 21
0x00, 0x00, 0x00, 0x00,   // line 22
0x00, 0x00, 0x00, 0x00,   // line 23
0x00, 0x00, 0x00, 0x00,   // line 24

0x00, 0x00, 0x00, 0x00,   // line 25
0x00, 0x00, 0x00, 0x00,   // line 26
0x00, 0x00, 0x00, 0x00,   // line 27
0x00, 0x00, 0x00, 0x00,   // line 28

0x00, 0x00, 0x00, 0x00,   // line 29
0x00, 0x00, 0x00, 0x00,   // line 30
0x00, 0x00, 0x00, 0x00,   // line 31
0x00, 0x00, 0x00, 0x00};  // line 32

void CVolumeControlDlg::InvalidateIcon()
{
  UINT nRgnStartIndx = 0;

  nRgnStartIndx =
    ( UINT )(( m_dwVolumeValue /( float )65536 ) * 20 );

  nRgnStartIndx += 4;
  nRgnStartIndx *= 4;

  nRgnStartIndx = 112 - nRgnStartIndx;

  BYTE* XORArray = new BYTE [ sizeof( XORmaskIcon ) ];
  memcpy( XORArray, XORmaskIcon, sizeof( XORmaskIcon ));

  for( UINT i = nRgnStartIndx + 1; i < nRgnStartIndx + 20; i += 4  )
  {
    XORArray[ i ] = 0xFF;
    XORArray[ i + 1 ] = 0xFF;
  }

  m_hIcon = CreateIcon(AfxGetInstanceHandle(), // application instance
                32,                            // icon width
                32,                            // icon height
                1,                             // number of XOR planes
                1,                             // number of bits per pixel
                ANDmaskIcon,                   // AND bitmask
                XORArray );                    // XOR bitmask

  SetIcon( m_hIcon, TRUE);                     // Set big icon

  SetIcon( m_hIcon, FALSE);                    // Set small icon

  if( m_pTrayIcon )
  {
    m_pTrayIcon->SetState();
  }
}

I would say getting the mixer controls would be as simple as sending mail. Like any other programming model in Windows (e.g. socket programming, stream input/output), you simply have to go through a set of standard steps. The steps would be something like open a mixer, get the mixer controls, and then set/get the volume level. And after use, close the mixer.

In my code, I have given wrappers for each of the above steps mentioned. The wrapper function has been called from the InitDialog function of my dialog class.

C++
BOOL CVolumeControlDlg::OnInitDialog()
{
    CDialog::OnInitDialog();

    // Add "About..." menu item to system menu.

    // IDM_ABOUTBOX must be in the system command range.

    ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
    ASSERT(IDM_ABOUTBOX < 0xF000);

    CMenu* pSysMenu = GetSystemMenu(FALSE);
    if (pSysMenu != NULL)
    {
        CString strAboutMenu;
        strAboutMenu.LoadString(IDS_ABOUTBOX);
        if (!strAboutMenu.IsEmpty())
        {
            pSysMenu->AppendMenu(MF_SEPARATOR);
            pSysMenu->AppendMenu(MF_STRING,
                       IDM_ABOUTBOX, strAboutMenu);
        }
    }

    SetWindowText( PROG_NAME );

    amdInitialize();
    amdGetMasterVolumeControl();
    amdGetMasterVolumeValue( m_dwVolumeValue );
    if( m_dwVolumeValue == 0 )
    {
        m_bIsMute = true;
    }

    InvalidateIcon();

    m_pTrayIcon = new SystrayIcon( this, WM_TRAY_NOTIFY,
                  "Volume Control",    &m_hIcon );
    m_pTrayIcon->SetState();

    m_pMouseHook = ( MouseHook* )
    AfxBeginThread( RUNTIME_CLASS( MouseHook ));

    m_pMouseHook->EnableHook( m_hWnd );

    return TRUE;
    // return TRUE  unless you set the focus to a control

}

The mixer control can be used to control any property of the mixer, e.g., system volume, wave volume, the line-in port etc. This is specified in the MIXERLINECONTROLS structure before passing this into ::mixerGetLineControls.

C++
BOOL CVolumeControlDlg::amdGetMasterVolumeControl()
{
    if (m_hMixer == NULL)
    {
        return FALSE;
    }

    // get dwLineID

    MIXERLINE mxl;
    mxl.cbStruct = sizeof(MIXERLINE);
    mxl.dwComponentType = MIXERLINE_COMPONENTTYPE_DST_SPEAKERS;
    if (::mixerGetLineInfo(reinterpret_cast<HMIXEROBJ>(m_hMixer),
        &mxl,
        MIXER_OBJECTF_HMIXER |
        MIXER_GETLINEINFOF_COMPONENTTYPE)
        != MMSYSERR_NOERROR)
    {
        return FALSE;
    }

    // get dwControlID

    MIXERCONTROL mxc;
    MIXERLINECONTROLS mxlc;
    mxlc.cbStruct = sizeof(MIXERLINECONTROLS);
    mxlc.dwLineID = mxl.dwLineID;
    //System volume control.

    mxlc.dwControlType = MIXERCONTROL_CONTROLTYPE_VOLUME;
    mxlc.cControls = 1;
    mxlc.cbmxctrl = sizeof(MIXERCONTROL);
    mxlc.pamxctrl = &mxc;
    if (::mixerGetLineControls(reinterpret_cast<HMIXEROBJ>(m_hMixer),
        &mxlc,
        MIXER_OBJECTF_HMIXER |
        MIXER_GETLINECONTROLSF_ONEBYTYPE)
        != MMSYSERR_NOERROR)
    {
        return FALSE;
    }

    // store dwControlID

    m_strDstLineName = mxl.szName;
    m_strVolumeControlName = mxc.szName;
    m_dwMinimum = mxc.Bounds.dwMinimum;
    m_dwMaximum = mxc.Bounds.dwMaximum;
    m_dwVolumeControlID = mxc.dwControlID;

    return TRUE;
}

The mute ON/OFF is handled inside OnMButtonUp( ). Ideally, this should be handled inside OnMButtonDown( ). But when I did so, I came across some issues like, this function is being called twice etc. So I thought of using OnMButtonUp( ). After that, when I thought in a little more conceptual way, I found that what I did was right. Therefore, I did not take the effort to rethink about it.

C++
void CVolumeControlDlg::OnMButtonUp( WPARAM wPrm, LPARAM lPrm )
{
  if( m_bIsMute )
  {
    m_bIsMute = false;
    amdSetMasterVolumeValue( m_dwPrevVolumeValue );
    amdGetMasterVolumeValue( m_dwVolumeValue );
    InvalidateIcon();
  }
  else
  {
    m_bIsMute = true;
    amdSetMasterVolumeValue( 0 );
    m_dwPrevVolumeValue = m_dwVolumeValue;
    amdGetMasterVolumeValue( m_dwVolumeValue );
  }
}

Handling Session Lock/Unlock

As stated earlier, only four music players are currently being supported and I'm on my feasibility works to support more of them.

The major challenge I had to face with this was getting the status of various music players. Each of the supported players required a different method to get it. For Jet Audio I used Windows Messages, for Winamp I got the status from the window title and for iTunes I used COM interfaces to get the status.

Please look into the code portions for details.

C++
//JET AUDIO
voidCVolumeControlDlg::PauseJetAudio( bool bMute /* = true */ )
{
    HWND hWndJetAudio = NULL;
    hWndJetAudio = ::FindWindow( "COWON Jet-Audio Remocon Class",
                                  "Jet-Audio Remote Control" );

    if( hWndJetAudio )
    {
        int nStatus = ::SendMessage( hWndJetAudio,
                                     WM_REMOCON_GETSTATUS,
                                     ( WPARAM )GetSafeHwnd(),
                                     GET_STATUS_STATUS );

        if( bMute )
        {
            if( MCI_MODE_PLAY == nStatus )
            {
                ::SendMessage( hWndJetAudio,
                               WM_REMOCON_SENDCOMMAND,
                               0,
                               MAKELPARAM( JRC_ID_PLAY, 0 ));

                m_bWasJetAudioMutedByUs = true;
            }
        }
        else
        {
            if( MCI_MODE_PLAY != nStatus &&
                m_bWasJetAudioMutedByUs )
            {
                ::SendMessage( hWndJetAudio,
                               WM_REMOCON_SENDCOMMAND,
                               0,
                               MAKELPARAM( JRC_ID_PLAY, 0 ));
            }

            m_bWasJetAudioMutedByUs = false;
        }
    }
}

//WINAMP
voidCVolumeControlDlg::PauseWinamp( boolbMute /* = true  */ )
{
    HWND hWndWinamp = NULL;
    hWndWinamp = ::FindWindow ( "Winamp v1.x", NULL );

    if( hWndWinamp )
    {
        CString csWinampText;

        ::GetWindowText( hWndWinamp, csWinampText.GetBuffer( 0 ), MAX_PATH );

        if( bMute )
        {
            if( -1 == csWinampText.Find( "[Stopped]" ) &&
                -1 == csWinampText.Find( "[Paused]" ) &&
                -1 != csWinampText.Find( "- Winamp" ))
            {
                ::SendMessage( hWndWinamp, WM_KEYDOWN, ( WPARAM )0x43, 0 );

                m_bWasWinampMutedByUs = true;
            }
        }
        else
        {
            if( m_bWasWinampMutedByUs &&
                ( -1 != csWinampText.Find( "[Paused]" )))
            {
                ::SendMessage( hWndWinamp, WM_KEYDOWN, ( WPARAM )0x43, 0 );
            }

            m_bWasWinampMutedByUs = false;
        }
    }
}

//iTUNES
voidCVolumeControlDlg::PauseiTunes( bool bMute /* = true  */ )
{
    IiTunes *iITunes = NULL;
    HRESULT hRes = -1;

    HWND hWndiTunes = NULL;
    hWndiTunes = ::FindWindow ( "iTunes", NULL );

    if( hWndiTunes )
    {
        CoInitialize( NULL );

        hRes = CoCreateInstance( CLSID_iTunesApp,
                                 NULL, CLSCTX_LOCAL_SERVER,
                                 IID_IiTunes, ( LPVOID* ) &iITunes );
    }

    if( hRes == S_OK )
    {
        ITPlayerState state = ITPlayerStateStopped;

        iITunes->get_PlayerState( &state );

        if( bMute )
        {
            if( ITPlayerStatePlaying == state )
            {
                iITunes->Pause();
                m_bWasiTunesMutedByUs = true;
            }
        }
        else
        {
            if( ITPlayerStateStopped == state &&
                m_bWasiTunesMutedByUs )
            {
                iITunes->Play();
            }

            m_bWasiTunesMutedByUs = false;
        }
    }
}

Along with pausing the playback of various music players, I'm also muting the system volume level.

For getting notifications of session change, subscribe for the event WM_WTSSESSION_CHANGE.

C++
ON_MESSAGE( WM_WTSSESSION_CHANGE, OnSessionChange )

Now the function OnSessionChange will be called for session changes. The WPARAM argument of this function will specify the type change of the session. In this function, I am only handling session lock and unlock.

C++
void CVolumeControlDlg::OnSessionChange( WPARAM wPrm, LPARAM lPrm )
{
    if( WTS_SESSION_LOCK == wPrm )
    {
        m_bIsMute = true;
    }
    else if( WTS_SESSION_UNLOCK == wPrm )
    {
        m_bIsMute = false;
    }

    PauseWinamp( m_bIsMute );
    PauseJetAudio( m_bIsMute );
    MuteVolume( m_bIsMute );
    RedrawIcon( m_bIsMute );
}

For implementing the COM interfaces, I had to download the iTunes COM Interface Library Apple iTunes. And for the message set for Jet Audio, I referred a sample application from Jet Audio's website. Getting the status for Winamp was pretty easy and this method is also the crudest among the lot. Winamp tags the player's status in the window title. So I read Winamp's window title and I searched for specific tags in it.

Open Issues

I could not get to work the COM interface for Windows Media Player. So this version of Volume Controller does not fully support Windows Media Player. The program will stop the playback when the system is locked, but will not resume it when the system is unlocked.

The issue I'm having for Windows Media Player is that I am unable to get the status of the player. To be precise, I could not find a way to know whether the player is paused or not. If that is possible, we can control the player using windows messages.

If somebody out there knows a solution for this, please do send it across to me.

Version History

  • r1 v2 - Controls the playback status of various music players with session Lock/Unlock
  • r1 v1 - Initial version - System volume could be controlled using the middle mouse button

r = release | v = version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Philips Electronics India Ltd
India India
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralMy vote of 5 Pin
unprime210-Nov-10 12:38
unprime210-Nov-10 12:38 
QuestionHmmm... Pin
Coder_200713-Nov-07 15:11
Coder_200713-Nov-07 15:11 
GeneralRe: Hmmm... Pin
Kiran Raj Joseph2-Jan-08 23:49
Kiran Raj Joseph2-Jan-08 23:49 
Generaldoesn't work... Pin
z0iid29-Jun-07 12:32
z0iid29-Jun-07 12:32 
GeneralRe: doesn't work... Pin
Kiran Raj Joseph2-Jan-08 23:56
Kiran Raj Joseph2-Jan-08 23:56 
GeneralRe: doesn't work... Pin
mbyamukama27-Sep-08 0:00
mbyamukama27-Sep-08 0:00 
GeneralRe: doesn't work... Pin
Kiran Raj Joseph8-Oct-08 17:36
Kiran Raj Joseph8-Oct-08 17:36 
GeneralFix Format Pin
NormDroid25-Jan-07 2:19
professionalNormDroid25-Jan-07 2:19 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.