This tutorial is going to be a three-part introduction to playing and recording audio files using winmm.dll (a WinAPI). This first part is about understanding, opening, and controlling Windows Mixers. The other tutorials will talk about recording and playing audio ".wav" files. We will not be working with .mp3 (you can see the documentation to understand those functions).
Figure 1: The Desktop with the app and Windows monitoring devices for the speaker and the microphone levels.
Let me start out with an apology. I am a techie and do not sing or play an instrument (over the years, I have earned a comfortable living NOT singing or playing an instrument). I have never been in a real recording studio but I think I know what it would be like. Also, I would like to say that understanding what a mixer is is almost straightforward. Actually, using the Windows Mixer is more like needing a suppository. Enough said, let me begin.
In my imaginary recording studio, there are input devices like microphones and guitar pickups. They do nothing until they are connected to an electronic device that can route that signal onto a recording media (Computer, Tape, Vinyl or whatever). There may also be output devices (Speakers, and Amplifiers) that need to be controlled as well.
Let's examine the Mixer concept. A very simple Mixer will a button for enabling each input or output device. It would also have a knob or slider to control the volume or level from, for each device. One of our mixers will have one slider control and one button control (volume and mute).
I am trying to explain something that may seem a little strange at first. Our mixers do not directly control a device. That is done by a master (system mixer), we only control the levels to and from that Master Mixer. This is because there may be other apps that are controlling levels to and from other available devices, some of which may be shared devices (like speakers). So, our app may be using the line-in source from a sound card and the on-board speakers (headphones). Another app may be taking notes by speaking into the on-board microphone while quietly playing them back through the same speaker (headphones). Our mixer will not be interested in that microphone. So in this scenario, it is much like having two studios that share one master mixer which actually controls all the devices but has several parallel inputs and outputs. Unfortunately, each app is only controlling the level that goes to the master mixer which, in turn, only fades or amplifies your sound and separately controls the sound from the other app. Each app gets its own mixer which in turn feeds the master mixer which controls the device.
The App
Before I actually begin, I have to admit that though the GUI code is mine and was recently written in C#, the Mixer code is something I wrote decades ago for an old audio player that I wrote in VB6. I do not think that mixer code is mine but I don't remember where I got it from but thanks to whoever actually wrote it.
Each Mixer class has one or more Control classes. Think of it this way - You have this machine in front of you. It has rows of buttons, VU Meters, and sliders that each control a given device. The Mixer Class is like the machine, the Control Classes are like the physical buttons and sliders and the electronics under them. The actual work is done here. There are mixers available for every enabled audio device that Windows can find.
Our app will have two mixers, each with two controls (Mute and Volume). One mixer will control output (one of the available speakers) and the other will control an input device (maybe the on-board microphone or a sound card line-in). Each mixer will probably have at least a volume and a mute control. You should be able to tell what state or level each control is in as well as the ability to set the state or level of the control. ( Like being able to see where the slider knob is relative to the top or bottom of its range as well as being able to move the knob to control the volume).
Mixer
We must ask for a list of all available mixers. Determine each mixer's purpose and then open each mixer we intend to use. Once a mixer is opened, we can see and use all available controls.
The first thing we need to do is to find out how many mixers ('devices') are available. They come in two major categories - input and output. We must first call MixerGetNumDevs directly from the winmm.dll.
- numDevs = MixerGetNumDevs(); ;
That will tell us the number of mixers available (all my laptops come stocked with just a microphone and a speaker. So numDevs==2 … one mic and one speaker mixer unless I add an external sound card. With the sound card, I get six mixers, four inputs and two outputs).
- uint MixerGetDevCaps(int mixerId, ref MIXERCAPS mixerCaps, int mixerCapsSize)
Now, we need to call MixerGetDevCaps, numDevs times with mixerid ranging from 0 to numDevs. Each call will fill in one MixerCap structure with information about that mixer including the mixer's name which we will use to determine how to open it. The order the caps comeback gives us an index to use to open a mixer. We store a list of those mixers in AllMixerCaps array.
Next, we need to get details about those mixers by calling waveInGetDevCaps and waveOutGetDevCaps. We must poll each function as many times as we have mixers. In this sample, we poll for output mixers. For input mixers as WAVEINCAPS and waveInGetDevCaps respectively, AllOutCaps and AllInCaps are the lists to be used elsewhere.
- private void GetSoundOutDevices()
- {
- uint devices = clsWinMMBase.waveOutGetNumDevs();
-
- AllOutCaps = new List<clsWinMMBase.WAVEOUTCAPS>();
- clsWinMMBase.WAVEOUTCAPS caps = new clsWinMMBase.WAVEOUTCAPS();
- {
- for (uint i = 0; i < devices; i++)
- {
-
- if (0 == clsWinMMBase.waveOutGetDevCaps(new IntPtr(i), ref caps, (uint)Marshal.SizeOf(typeof(clsWinMMBase.WAVEOUTCAPS))))
- {
-
- AllOutCaps.Add(caps);
- }
- }
- }
- }
If the function returns 0, it will have filled in the respective CapStructure. Please see MS documentation for details.
- public struct WAVEOUTCAPS
- {
- public ushort wMid;
- public ushort wPid;
- public uint vDriverVersion;
- [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 164)]
- public string szPname;
- public uint dwFormats;
- public ushort wChannels;
- public ushort wReserved1;
- public uint dwSupport;
- }
This structure will give us the name of the mixer (IE: Speaker Name or Input Device Name) - the information we will enter into the appropriate input or output combo box.
Now, we must open each individual mixer we want to use before we can use or see any of the controls. Remember the AllMixerCaps we filled in earlier. It is a list that has all mixer names.
We separated them according to input or output type by calling waveInGetDevCaps & waveOutGetDevCaps and putting their names in the appropriate combo box. Under the cmbInDevices and cmbOutDevices.SelectedIndexChange we open the proper mixer by finding that mixer by name in AllMixerCaps and passing that array index to mixerOpen as id. If the function succeeds, hMixer is a handle for all future use of the mixer.
- public bool OpenMixer(int id)
- {
- bool rv=false;
- int ArrayIndex;
-
- int open = mixerOpen(ref hMixer,(uint) id, IntPtr.Zero, IntPtr.Zero, 0);
- rv=(0==open);
- ...
- more code
- ...
- return rv;
- }
When the mixer is open, you need to find out how many controls there are, by calling mixerGetLineInfo (IntPtr hmxobj, ref MIXERLINE pmxl, UInt32 fdwInfo).
The pmxl structure will give you some information about the controls but mostly the number of controls (level and/or mute). To get further information, call mixerGetLineControls repeatedly setting dwControlID to every Boolean and Fader types:
- public bool OpenMixer(int id)
- {
- bool rv=false;
- int ArrayIndex;
-
- int open = mixerOpen(ref hMixer,(uint) id, IntPtr.Zero, IntPtr.Zero, 0);
- rv=(0==open);
- ...
- more code
- ...
- return rv;
- }
-
- //When the mixer is open you need to find out how many controls there are by calling mixerGetLineInfo(IntPtr hmxobj, ref MIXERLINE pmxl, UInt32 fdwInfo)
- // The pmxl structure will give you some information about the controls but mostly the number of controls (level and/or mute). To get further information, call mixerGetLineControls repeatedly setting dwControlID to every Boolean and Fader types:
-
- for (uint i = MIXERCONTROL_CONTROLTYPE_BOOLEAN; i <= MIXERCONTROL_CONTROLTYPE_STEREOENH; i++)
- {
- mlc.dwControlID = i;
- resp = mixerGetLineControls(hMixer, ref mlc, 2);
- if (resp != 0)
- {
-
- rv = mciGetErrorString(resp, errmsg, (uint)errmsg.Capacity);
- }
- else
- {
-
- mc = (MIXERCONTROL)Marshal.PtrToStructure(mlc.pamxctrl, typeof(MIXERCONTROL));
- GrowTheArray(ref MixerControls,mlc.cControls);
- ArrayIndex = MixerControls.Length-1;
-
-
- MixerControls[ArrayIndex] = new clsControl(hMixer, (uint)(ArrayIndex), mlc, mlc.cControlType .ToString (), iChannels);
-
- rv = true;
- }
- }
-
- for (uint i = MIXERCONTROL_CONTROLTYPE_FADER; i <= MIXERCONTROL_CONTROLTYPE_EQUALIZER; i++)
- {
- mlc.dwControlID = i;
- resp = mixerGetLineControls(hMixer, ref mlc, 2);
- if (resp != 0)
- {
-
- rv = mciGetErrorString(resp, errmsg, (uint)errmsg.Capacity);
- }
- else
- {
- mc = (MIXERCONTROL)Marshal.PtrToStructure(mlc.pamxctrl, typeof(MIXERCONTROL));
- GrowTheArray(ref MixerControls, mlc.cControls);
- ArrayIndex = MixerControls.Length-1;
- MixerControls[ArrayIndex] = new clsControl(hMixer, (uint)(ArrayIndex), mlc, mlc.cControlType .ToString (), iChannels);
- rv = true;
- }
- }
Store all control lines in the MixerControls array for future use. Now, the mixer is open and we can now use it. In our main form, we have one instance each for InputDevice and OutputDevice, each as clsMixer. They were instantiated and opened the associated Windows Mixer under the appropriate combo box index change. That clsMixer exposes methods for examining how many controls are available, what type of control it is, and methods for seeing the current state/value and setting said state/value. When we opened the mixer, we called mixerGetLineInfo which gave us the number of controls per mixer. When we repeatedly called mixerGetLineControls, we filled in a mixer control structure which is then used to instantiate a new clsControl which InputDevice and OuputDevice class use with exposed methods for using the mixer. The GUI only sees clsMixer but never actually exposes clsControl. Just as the audio engineer sees buttons and sliders but may not even know how they work.
Control
The Control class (clsControl) does the real work with -
Control Name: mixerGetLineControls
Control Minimum Value: mixerGetLineControls
Control Maximum Value: mixerGetLineControls
Number of Channels: mixerGetLineControls
Get Control Value: mixerGetControlDetails
Set Control Value : mixerSetControlDetails
When we called mixerGetLineInfo, a MIXERLINE structure was filled in. That gave us a name, the minimum and maximum values for the control and the number of Channels for that control.
Important Note: When you change the volume of the speaker, you are setting the value relative to the current level of the master mixer speaker level control. If you want to see what you are doing, open the Windows Mixer panel (typically: right click on the tray speaker icon and select mixer from the popup menu. As you can see in this figure, there is a Device mixer, a System Sounds mixer, and Audio_Recorder_w_Mixer_Controls mixer.
Changing the app speaker mixer volume slider only moves Audio_Recorder_w_Mixer_Controls slider between zero and the level set by the Device (Master) mixer level.
For input devices, you have to open the property window for the selected device (usually from the control panel).
The MIXERLINE structure was filled in when we called mixerGetLineInfo. From this structure, we know the number of connections, the number of channels and the number of lines available from the mixer. Most of this is mostly informational in this app. I am sure that this information is useful but not for our purpose.
Calling mixerGetLineControls fills in a MIXERLINECONTROLS structure. That structure contains a pointer to a MIXERCONTROL structure. That structure contains things like the min and max control value, a control id for use in working with the control.
We now must fill in our own MIXERCONTROLDETAILS structure to pass to mixerGetControlDetails.
- public struct MIXERCONTROLDETAILS
- {
- public UInt32 cbStruct;
- public UInt32 dwControlID;
- public UInt32 cChannels;
-
- public IntPtr hwndOwner;
- public UInt32 cMultipleItems;
-
- public UInt32 cbDetails;
- public IntPtr paDetails;
- }
Fill this structure in using the Control Id for dwControlID. Call mixerGetControlDetails. If that succeeds, the paDetails can be cast as MIXERCONTROLDETAILS_UNSIGNED (really an unsigned int) that contains the value or state for that control.
Likewise, to set the control, use mixerSetControlDetails. Fill in the structure using the Control Id for dwControlID and paDetails is a pointer to the unsigned int result. You must pass a value that is between the min and max obtained earlier.
That's all folks!
“Always keep searching but beware of what you search for. It may be searching for you.”