Steve Roman is a guest columnist for Dan Appleman this month, and I think you'll see why Dan graciously suggested he cede his space to Professor Roman this issue. In this article, Steve will show you how to work with the Windows mixer services—services that let you do some fundamental things with sound. Unfortunately, like many aspects of Windows, mixer services are made to appear unduly complicated by very poor documentation.
If you want to do multimedia programming, sooner or later (probably sooner) you'll need to deal with the Windows mixer services. These services enable you to do some fundamental things with sound, such as control the volume, bass, treble, and balance for the speakers.
If you'd asked me a few weeks ago if I ever thought I'd do any multimedia programming, I probably would have said "no." However, I recently acquired a wonderful program called The Pianist (from PG Music). The Pianist consists primarily of a collection of classical piano music pieces in MIDI file format, but, unfortunately, the accompanying application for playing these files simply didn't work correctly at high screen resolution (at least on my system). The result? I simply had to write my own program for playing the files. At a minimum, I wanted it to include a volume control, so I needed to get "mixed up" with my mixer.
Yes, but what is a mixer?Well, they say that a picture is worth a thousand words, and I think it's especially true here (see Figure 1).
Figure 1: Basic Mixer Services
Simply put, a mixer is a device for controlling audio lines. There are two types of audio lines—source lines and destination lines. Each source line brings a signal into the mixer from a source component, and each destination line takes a signal from the mixer to a destination component. Note that several source lines can be connected to a single destination line. For instance, in my case, the volume control line has six source connections.
Each audio line has an associated set of controls (not depicted in Figure 1) that are used to control the properties of the line. For instance, the Volume Control line, which provides signal to the speakers, has five controls (note the unfortunate double use of the word "control"): Master Volume Level, Master Base Level, Master Treble Level, Master Mute, and Playback Gain Control.
Controls are accessed through user-defined data types, which I'll refer to as structures. Generally speaking, to get information about a control (for example, to get or set its value), you define variables of appropriate structure types, initialize some of the member fields, then call some Windows API mixer functions that fill in remaining member fields.
Mixer API functions and structuresMercifully, there are only a handful of mixer-related API functions and structures, and Table 1 lists them by category. Items in all capital letters are structures; the other items are functions.
Table 1. Mixer-related API functions and structures.
Getting Number of Mixer Devices
mixerGetNumDevs
Get Mixer Information
mixerGetDevCaps
MIXERCAPS
Opening or Closing a Mixer
mixerClose
mixerOpen
Retrieving Mixer Identifiers
mixerGetID
Retrieving Audio Line Information
mixerGetLineInfo
MIXERLINE
MM_MIXM_CONTROL_CHANGE
MM_MIXM_LINE_CHANGE
Retrieving Line Controls
mixerGetLineControls
MIXERCONTROL
MIXERLINECONTROLS
Changing Control Attributes
mixerGetControlDetails
mixerSetControlDetails
MIXERCONTROLDETAILS
MIXERCONTROLDETAILS_BOOLEAN
MIXERCONTROLDETAILS_LISTTEXT
MIXERCONTROLDETAILS_SIGNED
MIXERCONTROLDETAILS_UNSIGNED
Sending User-Defined Messages
mixerMessage
The mixer structures
The best way to get an overview of the mixer services is to follow the structures. The MIXERCAPS structure holds information about the capabilities of a mixer (I'll discuss this in more detail later in the article). Figure 2 shows the remaining structures, along with their relationships and some of their data fields. Let's start by looking at the relationships between these structures.
A MIXERLINE structure, which holds information about a particular audio line, contains a LineID field that uniquely identifies the audio line. This field provides a link to a MIXERLINECONTROLS structure whose main purpose is to contain a pointer to (that is, address of) an array of MIXERCONTROL structures—one for each control that the audio line supports.
Each MIXERCONTROL structure has a ControlID field, which points to a MIXERCONTROLDETAILS structure. The purpose of this structure is to contain a pointer to an array of structures, each of which holds the values of a control (at last!). There's one such structure for each channel of a control (stereo audio lines have two channels, for instance.) This structure is one of the following, depending on the data type of the control's values:
MIXERCONTROLDETAILS_UNSIGNED
MIXERCONTROLDETAILS_SIGNED
MIXERCONTROLDETAILS_BOOLEAN
MIXERCONTROLDETAILS_LISTTEXT
For instance, a volume control has the value range of 0–65535, which requires an unsigned integer, and a mute control is either on or off and therefore assumes a Boolean value (range {0,1}).
Mixer structure dataFigure 2 also shows most (but not all) of the data that the mixer structures hold. As mentioned earlier, typically, some of this data is initialized before an appropriate mixer service is called to fill in the remaining data. For instance, in all cases except MIXERCONTROL, you must initialize a field that gives the size of the structure itself. Most of the data fields in Figure 2 are pretty straightforward, but I'll go over a few quirks related to using these structures in VB a bit later in the article.
The mixer servicesNow let's take a quick tour of the mixer services themselves—that is, the mixer API functions shown in Figure 2. Generally, the first step in using these services is to call mixerGetNumDevs, which takes no arguments and returns the number of mixers on the system. Usually this number will be one, but if it's zero, then it's time to pack it in! Mixers are numbered from 0 to one less than the number of mixers on the system. A mixer number is officially called a mixer identifier.
Figure 2: Mixer Structures
Getting mixer capabilitiesThe capabilities of a mixer are obtained by calling mixerGetDevCaps, whose declaration is:
Declare Function mixerGetDevCaps Lib "winmm.dll" Alias_
"mixerGetDevCapsA" (ByVal uMxId As Long, pmxcaps As_
MIXERCAPS, ByVal cbmxcaps As Long) As Long
This function takes a mixer identifier, a MIXERCAPS mixer structure (passed by reference), and the size (in bytes) of the MIXERCAPS structure. Here's the MIXERCAPS structure:
Type MIXERCAPS
wMid As Integer ' manufacturer id
wPid As Integer ' product id
vDriverVersion As Long ' version of the driver
szPname As String * MAXPNAMELEN ' product name
fdwSupport As Long ' misc. support bits
cDestinations As Long ' count of destinations
End Type
On my system, szName returns the string "SB16 Mixer [220]" because I have a Sound Blaster 16-bit sound card.
The MIXERCAPS structure requires no initialization prior to use. Here's the code to get the capabilities of mixer 0 and display the name and destination line count:
' Get caps for mixer 0
Dim uMixerCaps As MIXERCAPS
Call mixerGetDevCaps(0, uMixerCaps, LenB(uMixerCaps))
msgbox = "Mixer: " & Trim0(uMixerCaps.szPname) & _
vbCrLf & "Destination count: " & _
Format$(uMixerCaps.cDestinations)
The function Trim0 is a little utility I wrote to return the left portion of a string, up to the first null character (ASCII 0). This is very useful when dealing with API functions.
Opening and closing a mixerBefore you can use a mixer, it must be opened with mixerOpen:
Declare Function mixerOpen Lib "winmm.dll" (phmx As _
Long, ByVal uMxId As Long, ByVal dwCallback As _
Long, ByVal dwInstance As Long, ByVal fdwOpen As _
Long) As Long
This function returns a mixer handle in phmx, for use with other mixer functions. You need to fill in the mixer ID (uMxId) before calling this function. The remaining parameters are used to notify an application when there's a change on one of the mixer's audio lines. Windows will send a message (one of MM_MIXM_ CONTROL_CHANGE or MM_MIXM_LINE_CHANGE) to the window whose handle is given in dwCallBack. However, VB users will just set the last three parameters to 0. (You could subclass a window and intercept these messages, but this is problematic in VB.) Thus, a call to open mixer 0 looks like this:
' Open mixer 0
Dim hmx as Long
sMixError = mixerOpen(hmx, 0, 0, 0, 0)
A mixer should be closed with mixerClose when it's no longer needed.
Getting audio line informationTo get information about a specific audio line, use mixerGetLineInfo:
Declare Function mixerGetLineInfo Lib "winmm.dll"
Alias "mixerGetLineInfoA" (ByVal hmxobj As Long,_
pmxl As MIXERLINE, ByVal fdwInfo As Long) As Long
This function takes a mixer handle, a MIXERLINE structure (by reference), and a flag. The flag can take on one of several values, the most useful of which are: MIXER_GETLINEINFOF_DESTINATION, to get information about a destination line, and MIXER_GETLINEINFOF_SOURCE, to get information about a source line that's connected to a given destination line. Indeed, this is the order in which to retrieve line information—first get info on a destination line, then get info on the source lines connected to that destination line. Here's the MIXERLINE structure:
Type MIXERLINE
cbStruct As Long ' size of MIXERLINE structure
dwDestination As Long ' zero based dest. index
dwSource As Long ' zero based source index
dwLineID As Long ' unique line id
fdwLine As Long ' information about line
dwUser As Long ' driver specific information
dwComponentType As Long ' component type
cChannels As Long ' # of channels line supports
cConnections As Long ' # of connections possible
cControls As Long ' # of controls at this line
szShortName As String * MIXER_SHORT_NAME_CHARS
szName As String * MIXER_LONG_NAME_CHARS
lpTarget As Target
End Type
Some initialization of this structure is necessary before calling mixerGetLineInfo. You must set cbStruct to the size of the structure and dwDestination to the index of the destination line (the index ranges from 0 to uMixerCaps.cDestinations-1). When requesting information for a source line, you must also set dwSource to the source index. As mentioned earlier, the flag fdwInfo determines whether the information retrieved is for a destination line or a source line. Here's the code to get some of the line information on source line iSrc, connected to destination line iDest. It uses the mixer handle hmx, retrieved from the previous mixerOpen call.
Dim uMixerLine as MIXERLINE
' Initialize MIXERLINE structure
uMixerLine.cbStruct = LenB(uMixerLine)
uMixerLine.dwDestination = iDest
uMixerLine.dwSource = iSrc
' Call mixerGetLineInfo with Source flag
Call mixerGetLineInfo(hmx, uMixerLine, _
MIXER_GETLINEINFOF_SOURCE)
' Gather info on the line
sOut = "Source name: " & Trim0(uMixerLine.szName) _
& " (" & Trim0(uMixerLine.szShortName) & ")" _
& vbCrLf & "Line ID: " & Hex$(uMixerLine.dwLineID) _
& vbCrLf & "Component type: " _
& Format(GetComponentType(uMixerLine._
dwComponentType)) & vbCrLf & "Control count: " & _
Format(uMixerLine.cControls)
In this code, GetComponentType is a routine (essentially a large SELECT CASE statement) that returns a textual description of a control's type based on dwComponentType, a long integer.
Getting info on line controlsNow that you've filled a MIXERLINE structure using mixerGetLineInfo, it's time to call mixerGetLineControls:
Declare Function mixerGetLineControls Lib "winmm.dll" _
Alias "mixerGetLineControlsA" (ByVal hmxobj As Long, pmxlc_
As MIXERLINECONTROLS, ByVal fdwControls As Long) As Long
This function takes a mixer handle, a pointer to a MIXERLINECONTROLS structure, and a flag that indicates how many controls are requested. You can request a single control by ID or type, or you can get all controls for the line by using the flag MIXER_ GETLINECONTROLSF_ALL. Here's the MIXERLINECONTROLS structure:
Type MIXERLINECONTROLS
cbStruct As Long ' size of MIXERLINECONTROLS
dwLineID As Long ' line id
dwControl As Long
cControls As Long ' count of controls to retrieve
cbmxctrl As Long ' size of one MIXERCONTROL
pamxctrl As Long ' ptr to MIXERCONTROL array
End Type
The dwControl field is used when retrieving a single control by ID or type. After the function call, pamxctrl points to an array of MIXERCONTROL structures that have the following form:
Type MIXERCONTROL
cbStruct As Long ' size of structure
dwControlID As Long
dwControlType As Long
fdwControl As Long
cMultipleItems As Long
szShortName(1 To MIXER_SHORT_NAME_CHARS) As Byte
szName(1 To MIXER_LONG_NAME_CHARS) As Byte
Bounds(1 To 6) As Long
Metrics(1 To 6) As Long
End Type
This structure gives lots of information about the control, including its name, ID, type, and bounds (minimum and maximum values for the control). As previously mentioned, there are a few VB-specific issues to consider when coding for mixerGetMixerLineInfo.
Visual Basic issuesThe mixer services are part of the Windows System Development Kit (SDK). As such, all functions and concomitant structures are defined in the C language. This raises certain issues when porting the code to VB. I won't go into a detailed discussion of these issues; suffice it to say that, when passing a structure (user-defined type) to an API function, VB will often make a copy of that structure in order to change any Unicode strings to ANSI format and to make any necessary boundary realignments for the members of the structure. It then passes the copy to the function. The reverse process is performed by VB when the function returns.
This has two implications for us. First, note that the pamxctrl member of the MIXERLINECONTROLS structure is an address of another structure, so you need that address. Unfortunately, VB isn't much help in getting addresses. (The AddressOf operator works only on functions, not on user-defined types.) To get the necessary address, I use a DLL function called agGetAddressForObject, which is available on the CD-ROM that accompanies Dan Appleman's book Visual Basic 5.0 Programmer's Guide to the Win32 API. However, you can't pass this function the MIXERCONTROL structure variable, because VB will pass it the address of the copy of that structure! Instead, you must pass it a numeric variable within the structure and use the returned address of that variable to calculate the address of the beginning of the structure. The variable must not be of type string, because VB also makes copies of strings before passing them onto API functions. Fortunately, the first member of MIXERCONTROL has a numeric data type, and its address is the same as the address of the structure itself. Hence, you can pass agGetAddressForObject the first member of the first MIXERCONTROL structure in the array:
uMixerLineControls.pamxctrl = _
agGetAddressForObject(uMixerControl(1).cbStruct)
The second issue is that the MIXERCONTROL structure contains two strings—szShortName and szName—so, again, we face a string-copying issue. However, in this case, VB doesn't make a copy of these strings, because it doesn't realize that the strings are being passed indirectly to the API function. Hence, the strings are in VB's Unicode format, but mixerGetMixerLineInfo is going to return ANSI strings. No good. The solution, however, is simple—just declare the two strings as byte arrays that can accept ANSI characters with aplomb. Listing 1 shows the code surrounding a call to mixerGetMixerLineInfo. This code also examines the returned MIXERCONTROL structures and saves the control ID for the first volume control. I'll use this ID in Listing 2.
Listing 1. Code to call mixerGetMixerLineInfo and to process MIXERCONTROL structures.
Dim uMixerLineControls As MIXERLINECONTROLS
Dim lVolumeID as Long
' Initialize MIXERLINECONTROLS structure
uMixerLineControls.cbStruct = CLng(LenB_
(uMixerLineControls))
uMixerLineControls.dwLineID = uMixerLine.dwLineID
uMixerLineControls.cControls = uMixerLine.cControls
' Dimension MIXERCONTROL array to proper size
ReDim uMixerControl(1 To uMixerLine.cControls) As _
MIXERCONTROL
uMixerLineControls.cbmxctrl = CLng(LenB(uMixerControl_
(1)))
' Get address of first member of MIXERCONTROL array
uMixerLineControls.pamxctrl = _
agGetAddressForObject(uMixerControl(1).cbStruct)
Call mixerGetLineControls(hmx, uMixerLineControls, _
MIXER_GETLINECONTROLSF_ALL))
For i = 1 To uMixerLine.cControls
' Control name
sName = ""
For j = 1 To MIXER_LONG_NAME_CHARS
sName = sName & Chr(uMixerControl(i).szName(j))
Next j
' Control type
lType = uMixerControl(i).dwControlType
If lType = MIXERCONTROL_CONTROLTYPE_VOLUME then
lVolumeID = uMixerControl(i).dwControlID
End If
Next i
Listing 2. Retrieving a volume control's volume.
Dim uDetails As MIXERCONTROLDETAILS
' Get current volume
' Fill in control details structure
uDetails.cbStruct = CLng(LenB(uDetails))
uDetails.dwControlID = lVolumeID
uDetails.cChannels = 1
' Above for uniform treatment of all channels
uDetails.item = 0
' Dimension array to hold control values
ReDim uUnsigned(1 To uDetails.cChannels) As _
MIXERCONTROLDETAILS_UNSIGNED
uDetails.cbDetails = LenB(uUnsigned(1))
' Address of first structure
uDetails.paDetails = agGetAddressForObject(uUnsigned(1)._
dwValue)
' Call get details
Call mixerGetControlDetails(hmx, uDetails, _
MIXER_GETCONTROLDETAILSF_VALUE))
MsgBox "Current volume: " & Format(uUnsigned(1).dwValue)
The denouement—getting/setting control values
Once you have the MIXERCONTROL structure for a control, you can get or set the control's values. This is a two-part operation. First, you must set up a MIXERCONTROLDETAILS_XXX structure of the correct data type, which can be determined by checking the documentation for the control type. For instance, a volume control requires a MIXERCONTROLDETAILS_ UNSIGNED structure:
Type MIXERCONTROLDETAILS_UNSIGNED
dwValue As Long
End Type
The simplicity of this, by now, can only come as a welcome relief. Of course, to set a control's value, you must fill the dwValue field. The second step is to set up a MIXERCONTROLDETAILS structure:
Type MIXERCONTROLDETAILS
cbStruct As Long ' size of structure
dwControlID As Long ' control id
cChannels As Long ' number of channels
item As Long ' hwndOwner or cMultipleItems
cbDetails As Long ' size of details_XXX struct
paDetails As Long ' pointer to details_XXX array
End Type
Then you can call mixerGetControlDetails or mixerSetControlDetails:
Declare Function mixerGetControlDetails Lib _
"winmm.dll" Alias "mixerGetControlDetailsA" (ByVal_
hmxobj As Long, pmxcd As MIXERCONTROLDETAILS, ByVal_
fdwDetails As Long) As Long
Declare Function mixerSetControlDetails Lib _
"winmm.dll" (ByVal hmxobj As Long, pmxcd As _
MIXERCONTROLDETAILS, ByVal fdwDetails As Long) _
As Long
To get a control's value, you use the flag MIXER_GETCONTROLDETAILSF_VALUE. To set the value, you use MIXER_SETCONTROLDETAILSF_ VALUE. Listing 2 shows the code to get the volume of a volume control; this code uses the value of lVolumeID from Listing 1.
Some simple mixer appsThe code snippets I've presented here can be used to construct an application that displays the features of a mixer on any PC, and you can download a simple application (see the sample output in Figure 3) from the Subscriber Downloads at www.pinpub.com/vbd.
Figure 3: Simple volume control interfaceMixer Count: 1
Mixer: SB16 Mixer [220], Destinations: 3
Info on Mixer 0 as follows
Destination: Volume Control (Vol)
*********************************************************
Line ID: FFFF0000 •• Component type: MIXERLINE_COMPONENTTYPE_DST_SPEAKERS
Line Flag: Active •• Source connection count: 6 •• Channel count: 2 •• Countrol count: 5
Target media: SB16 Aux: Master [220] •• Type: MIXERLINE_TARGETTYPE_AUX
(1) CONTROL: Master Volume Level
Type: MIXERCONTROL_CONTROLTYPE_VOLUME
Control flag: •• Multiple items: 0 •• Control ID: 17 •• Bounds. Min: 0 Max: 65535 •• Current: 29018
(2) CONTROL: Master Bass Level
Type: MIXERCONTROL_CONTROLTYPE_BASS
Control flag: •• Multiple items: 0 •• Control ID: 22 •• Bounds. Min: 0 Max: 65535
(3) CONTROL: Master Treble Level
Type: MIXERCONTROL_CONTROLTYPE_TREBLE
Control flag: •• Multiple items: 0 •• Control ID: 23 •• Bounds. Min: 0 Max: 65535
(4) CONTROL: Master Mute
Type: MIXERCONTROL_CONTROLTYPE_MUTE
Control flag: Uniform •• Multiple items: 0 •• Control ID: 34 •• Bounds. Min: 0 Max: 1
(5) CONTROL: Playback Gain Control
Type: MIXERCONTROL_CONTROLTYPE_UNSIGNED
Control flag: •• Multiple items: 0 •• Control ID: 41 •• Bounds. Min: 0 Max: 3
SOURCE: Wave (Wave)
**********************************
Source Line ID: 0 •• Component type: MIXERLINE_COMPONENTTYPE_SRC_WAVEOUT
Line Flag: Source •• Source connection count: 0 •• Channel count: 2 •• Control count: 3
Target Media: SB16 Wave Out [220] •• Type: MIXERLINE_TARGETTYPE_WAVEOUT
(1) CONTROL: Wave Output Volume Level
Type: MIXERCONTROL_CONTROLTYPE_VOLUME
Control flag: •• Multiple items: 0 •• Control ID: 12 •• Bounds. Min: 0 Max: 65535 •• Current: 26214
(2) CONTROL: Wave Output Meter
Type: MIXERCONTROL_CONTROLTYPE_PEAKMETER
Control flag: •• Multiple items: 0 •• Control ID: 26 •• Bounds. Min: -32768 Max: 32767
(3) CONTROL: Wave Output Mute
Type: MIXERCONTROL_CONTROLTYPE_MUTE
Control flag: Uniform •• Multiple items: 0 •• Control ID: 29 •• Bounds. Min: 0 Max: 1
SOURCE: MIDI (MIDI)
**************************************
Source Line ID: 10000 •• Component type: MIXERLINE_COMPONENTTYPE_SRC_SYNTHESIZER
Line Flag: Source •• Source connection count: 0 •• Channel count: 2 •• Control count: 1
Target Media: Creative Music Synth [220] •• Type: MIXERLINE_TARGETTYPE_MIDIOUT
(1) CONTROL: MIDI Volume Level
Type: MIXERCONTROL_CONTROLTYPE_VOLUME
Control flag: •• Multiple items: 0 •• Control ID: 13 •• Bounds. Min: 0 Max: 65535 •• Current 33924
SOURCE: CD Audio (CD)
**************************************
Source Line ID: 20000 •• Component type: MIXERLINE_COMPONENTTYPE_SRC_COMPACTDISC
Line Flag: Source •• Source connection count: 0 •• Channel count: 2 •• Control count: 2
Target Media: SB16 Aux: CD [220] •• Type: MIXERLINE_TARGETTYPE_AUX
(1) CONTROL: CD Audio Volume Level
Type: MIXERCONTROL_CONTROLTYPE_VOLUME
Control flag: •• Multiple items: 0 •• Control ID: 14 •• Bounds. Min: 0 Max: 65535 •• Current 32768
(1) CONTROL: CD Audio Mute
Type: MIXERCONTROL_CONTROLTYPE_MUTE
Control flag: Uniform •• Multiple items: 0 •• Control ID: 31 •• Bounds. Min: 0 Max: 1
I've also included a program that creates a master volume control and master mute control for the speakers.
FinisI hope this quick tour of the mixer services has removed some of the obfuscation surrounding this area of multimedia programming and has convinced you to try your hand at mixing it up with the mixer. If you decide to give it a try, your first source of information (after this article, that is) will probably be the WIN32API.txt file that comes with VB. It has a section on mixer services, complete with with declarations of relevant functions, constants, and types. Remember, though (as Dan has so often pointed out): It has some errors. In particular, the declarations of MIXERCONTROL and MIXERLINECONTROLS should be replaced by the declarations in this article. Enjoy, but remember—it's a noisy world, so try to keep the volume down!
Download sample code here.Steven Roman is a professor of mathematics at the California State University at Fullerton. He's written 28 books, three of which are in the area of personal computing. In particular, he's the author of Concepts of Object-Oriented Programming with Visual Basic (Springer-Verlag) and Access Database Design and Programming (O'Reilly). He's also the author of the as-yet-unpublished book, Understanding Personal Computer Hardware, an In-Depth Look at PC Hardware. sroman@fullerton.edu.