In this tutorial I will be showing you how you would go about
controlling your system volume in C#. The reason for this tutorial is
simple, Ive been asked too many times how to do it, so I thought it
appropriate to write a tutorial showing how to do this task. Before we
get into any code, you need to understand that this tutorial in an
advanced tutorial, using Win32 API calls, custom structures, and the
like. If you are not familiar with those items, I suggest you study them
before attempting this tutorial.
Controlling your system volume takes a lot of advanced coding, because
you have to hook into the operating system in order to gain access to
the controls needed, thats the reason for all the Win32 API's. The
easiest way to do this is by using a mixer control, which is what we
will be doing in this tutorial. This tutorial may be rather long, as
this is a complete class library, with class files for:
- All the Win32 API Calls
- All the structures
- All the constants needed
- Class for doing the actual work
I will start by showing the other 3 classes first, so you can see first
hand the items needed for taking control of the system's volume
controls.
The first class we will see is the Win32 API class. In this class is
where we will be storing all the Win32 calls needed for controlling the
sound. Since each section is it's own class, we will need to reference
certain Namespaces for each class. The Namespaces we need for the Win32
class are:
2 | using System.Collections.Generic; |
4 | using System.Runtime.InteropServices; |
6 | using VolumeControl.Library.Constants; |
7 | using VolumeControl.Library.Structs; |
You will notice there are 2 custom Namespaces, those are our other class
files we will be getting to shortly. Since our classes reference custom
Namespaces, you should have guessed that all the class are a member of
the
VolumeControl.Library Namespace, but each also have an appendage to that, depending on which class it is. The Namespaces we create are:
- VolumeControl.Library.Structs
- VolumeControl.Library.Win32
- VolumeControl.Library.Constants
The Namespace for the Win32 class is, you guessed it,
VolumeControl.Library.Win32,
I am going to make an assumption here, since you continued to read
along in this tutorial I'm assuming you know about Win32 API calls, and
how to look up their function if you needed to, so I am not going to go
into great detail about what these API calls do. If you want to look
them up, a good place to start is
PInvoke.Net, they have pretty much every Win32 API in programming.
In this class we have Win32 API's for opening a mixer control, closing a
mixer control, getting the details of the current mixer control,
getting the ID of the mixer control, and more. Here are the Win32 API's
we will be needing:
- mixerClose
- mixerGetControlDetailsA
- mixerGetDevCapsA
- mixerGetID
- mixerGetLineControlsA
- mixerGetLineInfoA
- mixerGetNumDevs
- mixerMessage
- mixerOpen
- mixerSetControlDetails
NOTE: You will notice that the first 3 class are all declared as
static,
this is done so we don't have to create an instance of the class to use
the items it contains. As with structs, there is a raging debate over
using static classes, as far as I'm concerned the jury is still out on
this.
Now on to the class that actually does all the work. In this class we
have 4 methods, 1 for getting the mixer control, 1 for setting the mixer
control, 1 for getting the current volume level, and finally 1 for
setting the volume level. First the method for getting the mixer
control. In this method we will be creating a new mixer control, we will
retrieve it and use the method for setting its properties to set all
the information we need for the mixer.
In this class you will be using many methods of the
Marshal Class, located in the
System.Runtime.InteropServices Namespace. We will be allocating memory blocks for our mixer control using the
AllocCoTaskMem Method, we will be grabbing the size of our unmanaged type using the
Marshal.SizeOf Method and more.
This is one of the reasons I said at the start of this tutorial that it
is an advanced tutorial, and requires a firm grasp of the language, and
the objects used, to understand what is going on. Before we can write
any code that is going to work, we need to make sure we have references
to the proper Namespaces, here are the Namespaces you will need to
reference:
| using System.Collections.Generic; |
| using System.Runtime.InteropServices; |
| using System.Windows.Forms; |
| using VolumeControl.Library.Constants; |
| using VolumeControl.Library.Structs; |
| using VolumeControl.Library.Win32; |
Now for the first method in our class, the
GetMixer method:
02 | /// method to retrieve a mixer control |
04 | /// <param name="i"></param> |
05 | /// <param name="type"></param> |
06 | /// <param name="ctrlType"></param> |
07 | /// <param name="mixerControl"></param> |
08 | /// <param name="currVolume"></param> |
09 | /// <returns></returns> |
10 | private static bool GetMixer( int i, int type, int ctrlType, out VolumeStructs.Mixer mixerControl, out int currVolume) |
18 | VolumeStructs.LineControls lineControls = new VolumeStructs.LineControls(); |
19 | VolumeStructs.MixerLine line = new VolumeStructs.MixerLine(); |
20 | VolumeStructs.MixerDetails mcDetails = new VolumeStructs.MixerDetails(); |
21 | VolumeStructs.UnsignedMixerDetails detailsUnsigned = new VolumeStructs.UnsignedMixerDetails(); |
24 | mixerControl = new VolumeStructs.Mixer(); |
27 | line.cbStruct = Marshal.SizeOf(line); |
28 | line.dwComponentType = type; |
30 | details = PCWin32.mixerGetLineInfoA(i, ref line, VolumeConstants.MIXER_GETLINEINFOF_COMPONENTTYPE); |
33 | if (VolumeConstants.MMSYSERR_NOERROR == details) |
37 | int control = Marshal.SizeOf( typeof (VolumeStructs.Mixer)); |
39 | lineControls.pamxctrl = Marshal.AllocCoTaskMem(mcSize); |
41 | lineControls.cbStruct = Marshal.SizeOf(lineControls); |
44 | lineControls.dwLineID = line.dwLineID; |
45 | lineControls.dwControl = ctrlType; |
46 | lineControls.cControls = 1; |
47 | lineControls.cbmxctrl = mcSize; |
50 | mixerControl.cbStruct = mcSize; |
53 | details = PCWin32.mixerGetLineControlsA(i, ref lineControls, VolumeConstants.MIXER_GETLINECONTROLSF_ONEBYTYPE); |
56 | if (VolumeConstants.MMSYSERR_NOERROR == details) |
60 | mixerControl = (VolumeStructs.Mixer)Marshal.PtrToStructure(lineControls.pamxctrl, typeof (VolumeStructs.Mixer)); |
67 | int mcDetailsSize = Marshal.SizeOf( typeof (VolumeStructs.MixerDetails)); |
68 | int mcDetailsUnsigned = Marshal.SizeOf( typeof (VolumeStructs.UnsignedMixerDetails)); |
69 | mcDetails.cbStruct = mcDetailsSize; |
70 | mcDetails.dwControlID = mixerControl.dwControlID; |
71 | mcDetails.paDetails = Marshal.AllocCoTaskMem(mcDetailsUnsigned); |
72 | mcDetails.cChannels = 1; |
74 | mcDetails.cbDetails = mcDetailsUnsigned; |
75 | details = PCWin32.mixerGetControlDetailsA(i, ref mcDetails, VolumeConstants.MIXER_GETCONTROLDETAILSF_VALUE); |
76 | detailsUnsigned = (VolumeStructs.UnsignedMixerDetails)Marshal.PtrToStructure(mcDetails.paDetails, typeof (VolumeStructs.UnsignedMixerDetails)); |
77 | currVolume = detailsUnsigned.dwValue; |
Next we will be looking at the
SetMixer method. This is the method that does all the work for changing the volume level. It is called from the
SetVolume method. We will be passing this method the mixer control we're using, the level we want the volume set at:
02 | /// method for setting the value for a volume control |
04 | /// <param name="i"></param> |
05 | /// <param name="mixerControl"></param> |
06 | /// <param name="volumeLevel"></param> |
07 | /// <returns>true/false</returns> |
08 | private static bool SetMixer( int i, VolumeStructs.Mixer mixerControl, int volumeLevel) |
15 | VolumeStructs.MixerDetails mixerDetails = new VolumeStructs.MixerDetails(); |
16 | VolumeStructs.UnsignedMixerDetails volume = new VolumeStructs.UnsignedMixerDetails(); |
19 | mixerDetails.item = 0; |
21 | mixerDetails.dwControlID = mixerControl.dwControlID; |
23 | mixerDetails.cbStruct = Marshal.SizeOf(mixerDetails); |
25 | mixerDetails.cbDetails = Marshal.SizeOf(volume); |
28 | mixerDetails.cChannels = 1; |
29 | volume.dwValue = volumeLevel; |
32 | mixerDetails.paDetails = Marshal.AllocCoTaskMem(Marshal.SizeOf( typeof (VolumeStructs.UnsignedMixerDetails))); |
33 | Marshal.StructureToPtr(volume, mixerDetails.paDetails, false ); |
36 | details = PCWin32.mixerSetControlDetails(i, ref mixerDetails, VolumeConstants.MIXER_SETCONTROLDETAILSF_VALUE); |
39 | if (VolumeConstants.MMSYSERR_NOERROR == details) |
Now before you can set the volume you need to know what the current volume is, that is the job of our
GetVolume method. Here we will call our
GetMixer
method which will allow us to grab the current volume level. Remember
to always close your mixer object when you are done using it to free up
those resources, and to prevent any memory leaks.
NOTE: .Net languages are managed languages
and do a pretty good job managing resources and memory usage, but here
we are dealing with an unmanaged type so we need to ensure we are
closing items when we dont need them anymore.
Now for the
GetVolume method:
02 | /// method for retrieving the current volume from the system |
04 | /// <returns>int value</returns> |
05 | public static int GetVolume() |
12 | VolumeStructs.Mixer mixer = new VolumeStructs.Mixer(); |
15 | PCWin32.mixerOpen( out mixerControl, 0, 0, 0, 0); |
18 | int type = VolumeConstants.MIXERCONTROL_CONTROLTYPE_VOLUME; |
21 | GetMixer(mixerControl, VolumeConstants.MIXERLINE_COMPONENTTYPE_DST_SPEAKERS, type, out mixer, out currVolume); |
24 | PCWin32.mixerClose(mixerControl); |
The final method we have is the method for setting the volume to a
specified level. Here we will create a new mixer control (as we do in
every method in this class, because we make sure to close it as soon as
we're done with it). We will then open the mixer, then we will check the
value that is being passed for the volume level. If a value greater
than the maximum level we will set the level at the maximum value, same
with the minimum level, if the value provided is lower than the minimum
value we will set the volume level at the minimum level. Once we are
done we, once again, close the mixer to free up those resources:
02 | /// method for setting the volume to a specific level |
04 | /// <param name="volumeLevel">volume level we wish to set volume to</param> |
05 | public static void SetVolume( int volumeLevel) |
14 | VolumeStructs.Mixer volumeControl = new VolumeStructs.Mixer(); |
17 | PCWin32.mixerOpen( out mixerControl, 0, 0, 0, 0); |
20 | int controlType = VolumeConstants.MIXERCONTROL_CONTROLTYPE_VOLUME; |
23 | GetMixer(mixerControl, VolumeConstants.MIXERLINE_COMPONENTTYPE_DST_SPEAKERS, controlType, out volumeControl, out currVolume); |
29 | if (volumeLevel > volumeControl.lMaximum) |
31 | volumeLevel = volumeControl.lMaximum; |
33 | else if (volumeLevel < volumeControl.lMinimum) |
35 | volumeLevel = volumeControl.lMinimum; |
39 | SetMixer(mixerControl, volumeControl, volumeLevel); |
42 | GetMixer(mixerControl, VolumeConstants.MIXERLINE_COMPONENTTYPE_DST_SPEAKERS, controlType, out volumeControl, out currVolume); |
45 | if (volumeLevel != currVolume) |
47 | throw new Exception( "Cannot Set Volume" ); |
51 | PCWin32.mixerClose(mixerControl); |
55 | MessageBox.Show(ex.Message); |
Now that we have created this class library you're probably wondering
how you actually use it. Well here is an example of how you would use
this in, say the click event of a button on your form. When we click the
button we will first display the current volume level, we will then set
a new volume level, then we will re-display the current volume level:
01 | private void button1_Click( object sender, System.EventArgs e) |
04 | Label1.Text = VolumeControl.GetVolume.ToString(); |
07 | VolumeControl.SetVolume(50); |
10 | Label1.Text = VolumeControl.GetVolume().ToString(); |
There you have it! A way to create a Windows mixer control and control
the volume of the computer the application is running on. I truly hope
you found this tutorial useful and informative, I know I learned a lot
when creating this class library. I am including the solution for this
class library, all the license information and headers must remain in
tact because it is convered by the
GNU - General Public License, but you are free to modify and distribute as you wish. Thank you for reading.