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.
BYTE ANDmaskIcon[] = {
0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F,
0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F,
0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F,
0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F,
0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F,
0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F,
0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F,
0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F, 0xF0, 0x00, 0x00, 0x0F};
BYTE XORmaskIcon[] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
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(), 32, 32, 1, 1, ANDmaskIcon, XORArray );
SetIcon( m_hIcon, TRUE);
SetIcon( m_hIcon, FALSE);
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.
BOOL CVolumeControlDlg::OnInitDialog()
{
CDialog::OnInitDialog();
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;
}
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
.
BOOL CVolumeControlDlg::amdGetMasterVolumeControl()
{
if (m_hMixer == NULL)
{
return FALSE;
}
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;
}
MIXERCONTROL mxc;
MIXERLINECONTROLS mxlc;
mxlc.cbStruct = sizeof(MIXERLINECONTROLS);
mxlc.dwLineID = mxl.dwLineID;
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;
}
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.
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.
voidCVolumeControlDlg::PauseJetAudio( bool bMute )
{
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;
}
}
}
voidCVolumeControlDlg::PauseWinamp( boolbMute )
{
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;
}
}
}
voidCVolumeControlDlg::PauseiTunes( bool bMute )
{
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
.
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.
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