In Chapter 2, Digital Audio, I briefly discussed mixers--electronic circuits that combine or add several signals together. The capabilities of mixers provided on sound cards vary. The /dev/mixer device (and /dev/mixer1, if a second mixer is supported) presents an idealized model of a sound card mixer; it is shown in block diagram form in Figure 14-3.
The mixer really contains two mixer circuits. The input mixer (shown near the bottom of the diagram) accepts analog inputs from a number of different signal sources. The sources are sometimes referred to as mixer channels or mixer devices. An electronic gain control, a software-controlled "volume control," adjusts the level of the signal from each mixer channel before it goes into the input mixer. Electronic switches control which channels have their signals connected to the mixer. Some sound cards only allow one channel to be connected as a recording source (the inputs are exclusive choices), while others allow any combination of inputs. The signals are then fed to the mixer, which essentially adds them together. There is usually one final gain control that adjusts the level of the signals coming out of the mixer (labelled Reclev, for recording level, in Figure 14-3). The resulting signal is fed to the analog to digital converter where it can be digitized for further signal processing (e.g., written to a sound file). Note that up until the analog to digital converter, all of the signals are in analog form.
The output mixer works in a similar manner. Various input signals are fed to the input mixer, usually passing through gain controls first. In this case all channels are normally connected to the mixer. To effectively remove an input signal from the mixer, its gain control should be set to zero gain. After the output mixer combines the analog signals, there is one final gain control to adjust the overall volume level, and there may be tone controls. The last step is to send the resulting output signal to the speakers or other analog outputs.
This is an idealized mixer; any or all of the inputs, outputs, and level controls may or may not be present. Some sound cards, most notably the original SoundBlaster, have no programmable mixer channels at all. The audio paths may be stereo, mono, or a mixture of both. As the capabilities and design of sound cards vary, there may be slight differences between the diagram and a specific card--for example, on some cards the CD level setting may affect both record and playback, while on others it is only a playback level control. Software applications should determine the capabilities of the mixer at run-time using calls to the sound driver, so that the applications are not dependent on the capabilities of any one sound card. I will illustrate how to do this shortly.
Programming the mixer consists of setting the desired levels for the gain controls and the switches for the recording source. Other than that, the mixer operates continuously, taking no computing resources to operate.
The mixer doesn't fit into the typical UNIX device model, and therefore does not support the read and write system calls. Other than open and close, all functions are performed using the ioctl call.
Unlike the DSP device, more than one process can open the mixer at one time. Furthermore, any mixer settings remain in effect after the mixer device is closed. This capability is desirable, because you generally want to be able to set a parameter, such as the volume level, and have it remain in effect after the program setting it has completed. When the kernel first initializes, the mixer is set to reasonable default values.
You can also take one shortcut when programming: the mixer ioctl calls can be used on any sound device (to access the first mixer only). For example, if an application has opened /dev/dsp, there is no need to open /dev/mixer to change mixer settings. Just use the file descriptor that was returned when you opened /dev/dsp.
All of the mixer ioctl commands are prefixed with SOUND_MIXER or MIXER_. Like the DSP device ioctl calls, the third parameter should be a pointer to an integer. The driver will return a value in this parameter.
The devices currently supported by the sound driver are shown in Table 14-1. The names in the first column are the symbolic names used as parameters to the ioctl system calls that control the mixer. The second column lists the purpose of each mixer channel.
| Name | Description |
|---|---|
| SOUND_MIXER_VOLUME | master output level |
| SOUND_MIXER_BASS | bass tone control |
| SOUND_MIXER_TREBLE | treble tone control |
| SOUND_MIXER_SYNTH | FM synthesizer |
| SOUND_MIXER_PCM | D/A converter |
| SOUND_MIXER_SPEAKER | PC speaker output level |
| SOUND_MIXER_LINE | line input |
| SOUND_MIXER_MIC | microphone input |
| SOUND_MIXER_CD | audio CD input |
| SOUND_MIXER_IMIX | playback volume from recording source |
| SOUND_MIXER_ALTPCM | secondary D/A converter |
| SOUND_MIXER_RECLEV | master recording level |
| SOUND_MIXER_IGAIN | input gain level |
| SOUND_MIXER_OGAIN | output gain level |
| SOUND_MIXER_LINE1 | card-specific input #1 |
| SOUND_MIXER_LINE2 | card-specific input #2 |
| SOUND_MIXER_LINE3 | card-specific input #3 |
The main function of a mixer is to set gain levels. Different sound cards may provide 8 or 16 bits of gain control. As an application programmer you do not have to worry about this; the sound driver scales all levels to a percentage, a value from 0 to 100. The macro MIXER_READ is the recommended way to read the current level setting of a channel. It accepts a parameter corresponding to the bitmask for the channel in question. For example, the call to read the current microphone input level could look like this:
int vol;
ioctl(fd, MIXER_READ(SOUND_MIXER_MIC), &vol);
printf("Mic gain is at %d %%\n", vol);
The channel may support stereo, so the returned volume includes two values, one for each channel. The least significant byte holds the left channel volume, and the next significant byte holds the right channel volume. Decoding can be performed like this:
int left, right;
left = vol & 0xff;
right = (vol & 0xff00) >> 8;
printf("Left gain is %d %%, Right gain is %d %%\n", left, right);
For mono devices (one channel), the gain value is in the lower order byte (the same as the left channel above).
The gain levels can be set using the MIXER_WRITE macro. The volume settings are encoded in the same manner as when reading, like this:
vol = (right << 8) + left; ioctl(fd, MIXER_WRITE(SOUND_MIXER_MIC), &vol);
The volume parameter passed to the ioctl is both an input and an output. As the capabilities of the mixer hardware channels vary, the sound driver will have to scale the percentage value to the nearest value supported by the hardware. The ioctl call returns the actual value used.
Most ioctl calls to the mixer either act on a mixer channel (one of the level controls shown in Table 14-1) or make use of a bit field containing information for all channels. The sound driver header file provides symbolic names for each of these channels. The actual names are subject to change during future development of the mixer driver, but the total number of channels will be equal to the value SOUND_MIXER_NRDEVICES (i.e., they will range from zero through SOUND_MIXER_NRDEVICES - 1). In addition, you can obtain symbolic names for these channels if you define arrays such as the following:
const char *labels[] = SOUND_DEVICE_LABELS; const char *names[] = SOUND_DEVICE_NAMES;
The first set of names are in a format suitable for labeling the controls of a mixer program. The second set are in a format better suited for command-line options (i.e., they are all single words in lowercase).
Several ioctl calls are used for finding out information about the mixer. These all return an integer bitmask in which each bit corresponds to a particular mixer channel. SOUND_MIXER_READ_DEVMASK returns a bitmask where a bit is set for each channel that is supported by the mixer. SOUND_MIXER_READ_RECMASK has a bit set for each channel that can be used as a recording source. For example, we could check if the CD input was a valid mixer channel using the following code:
ioctl(fd, SOUND_MIXER_READ_DEVMASK, &devmask);
if (devmask & SOUND_MIXER_CD)
printf("The CD input is supported");
We could also find out if it was available as a recording source using this code:
ioctl(fd, SOUND_MIXER_READ_RECMASK, &recmask);
if (recmask & SOUND_MIXER_CD)
printf("The CD input can be a recording source");
Remember to use the bitwise operator (&) here, not the boolean operator (&&).
SOUND_MIXER_READ_RECSRC indicates which channels are currently selected as the recording source. More than one source may be selected, if the sound card permits. SOUND_MIXER_READ_STEREODEVS has bits set if a channel supports stereo. If cleared, it supports only one channel (mono).
A similar ioctl call returns information about the sound card as a whole: SOUND_MIXER_READ_CAPS. Each bit corresponds to a capability of the sound card. Currently only one capability exists: SOUND_CAP_EXCL_INPUT. If this bit is set, the recording source channels are mutually exclusive choices. If cleared, then any or all can be set at one time.
The ioctl SOUND_MIXER_WRITE_RECSRC sets the current recording source channel. Following the earlier example, we could now set the CD input as a recording source using:
devmask = SOUND_MIXER_CD; ioctl(fd, SOUND_MIXER_WRITE_DEVMASK, &devmask);
Make sure you pass a variable as the argument. Passing an immediate value won't work because the kernel function expects a pointer.
The sample program shown in Example 14-3 illustrates most of the functions of the mixer. It lists all of the mixer channels, indicating which are available on the currently installed sound card. It shows which channels can be inputs, which input channels are currently selected, whether the channels are stereo, and the current gain setting. It also lists whether the input channels are a mutually exclusive choice. If the input channels are mutually exclusive, then only one input channel may be selected at any one time. If the choice is not mutually exclusive, then you may select more than one simultaneous input source channel.Accessing /dev/mixer
/*
* mixer_info.c
* Example program to display mixer settings
*/
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <linux/soundcard.h>
/* utility function for printing status */
void yes_no(int condition)
{
condition ? printf(" yes ") : printf(" no ");
}
int main(int argc, char *argv[])
{
int fd; /* file descriptor for mixer device */
int i; /* loop counter */
int level; /* volume setting */
char *device; /* name of device to report on */
int status; /* status of system calls */
/* various device settings */
int recsrc, devmask, recmask, stereodevs, caps;
/* names of available mixer channels */
const char *sound_device_names[] = SOUND_DEVICE_LABELS;
/* get device name from command line or use default */
if (argc == 2)
device = argv[1];
else
device = "/dev/mixer";
/* open mixer, read only */
fd = open(device, O_RDONLY);
if (fd == -1) {
fprintf(stderr, "%s: unable to open `%s', ", argv[0], device);
perror("");
return 1;
}
/* get all of the information about the mixer */
status = ioctl(fd, SOUND_MIXER_READ_RECSRC, &recsrc);
if (status == -1)
perror("SOUND_MIXER_READ_RECSRC ioctl failed");
status = ioctl(fd, SOUND_MIXER_READ_DEVMASK, &devmask);
if (status == -1)
perror("SOUND_MIXER_READ_DEVMASK ioctl failed");
status = ioctl(fd, SOUND_MIXER_READ_RECMASK, &recmask);
if (status == -1)
perror("SOUND_MIXER_READ_RECMASK ioctl failed");
status = ioctl(fd, SOUND_MIXER_READ_STEREODEVS, &stereodevs);
if (status == -1)
perror("SOUND_MIXER_READ_STEREODEVS ioctl failed");
status = ioctl(fd, SOUND_MIXER_READ_CAPS, &caps);
if (status == -1)
perror("SOUND_MIXER_READ_CAPS ioctl failed");
/* print results in a table */
printf(
"Status of %s:\n\n"
"Mixer Device Recording Active Stereo Current\n"
"Channel Available Source Source Device Level\n"
"--------- --------- --------- -------- --------- ---------\n",
device
);
/* loop over all devices */
for (i = 0 ; i < SOUND_MIXER_NRDEVICES ; i++) {
/* print number and name */
printf("%2d %-7s", i, sound_device_names[i]);
/* print if available */
yes_no((1 << i) & devmask);
/* can it be used as a recording source? */
yes_no((1 << i) & recmask);
/* it it an active recording source? */
yes_no((1 << i) & recsrc);
/* does it have stereo capability? */
yes_no((1 << i) & stereodevs);
/* if available, display current level */
if ((1 << i) & devmask) {
/* if stereo, show both levels */
if ((1 << i) & stereodevs) {
status = ioctl(fd, MIXER_READ(i), &level);
if (status == -1)
perror("SOUND_MIXER_READ ioctl failed");
printf(" %d%% %d%%", level & 0xff, (level & 0xff00) >> 8);
} else { /* only one channel */
status = ioctl(fd, MIXER_READ(i), &level);
if (status == -1)
perror("SOUND_MIXER_READ ioctl failed");
printf(" %d%%", level & 0xff);
}
}
printf("\n");
}
printf("\n");
/* are recording sources exclusive? */
printf("Note: Choices for recording source are ");
if (!(caps & SOUND_CAP_EXCL_INPUT))
printf("not ");
printf("exclusive.\n");
/* close mixer device */
close(fd);
return 0;
}
Note how the sample program avoids hardcoding any particular channel names or numbers in the source. This independence keeps the program portable as new channels are added to the sound driver. On my system, this output is produced:
Status of /dev/mixer: Mixer Device Recording Active Stereo Current Channel Available Source Source Device Level --------- --------- --------- -------- --------- --------- 0 Vol yes no no yes 90% 90% 1 Bass no no no no 2 Trebl no no no no 3 Synth yes no no yes 75% 75% 4 Pcm yes no no yes 75% 75% 5 Spkr no no no no 6 Line yes yes no yes 75% 75% 7 Mic yes yes yes no 16% 8 CD yes yes no yes 75% 75% 9 Mix no no no no 10 Pcm2 no no no no 11 Rec no no no no 12 IGain no no no no 13 OGain no no no no 14 Line1 no no no no 15 Line2 no no no no 16 Line3 no no no no Note: Choices for recording source are exclusive.
You can verify that the mixer functions also operate using another sound device by running the program with /dev/dsp as the device file given on the command line.
The previous program is instructive, but not particularly useful because it does not allow you to change the mixer settings. Example 14-4 is a simple program that allows setting the mixer levels.Simple Mixer Program
/*
* mixer.c
* Example of a simple mixer program
*/
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <linux/soundcard.h>
/* names of available mixer devices */
const char *sound_device_names[] = SOUND_DEVICE_NAMES;
int fd; /* file descriptor for mixer device */
int devmask, stereodevs; /* bit masks of mixer information */
char *name; /* program name */
/* display command usage and exit with error status */
void usage()
{
int i;
fprintf(stderr, "usage: %s <device> <left-gain%%> <right-gain%%>\n"
" %s <device> <gain%%>\n\n"
"Where <device> is one of:\n", name, name);
for (i = 0 ; i < SOUND_MIXER_NRDEVICES ; i++)
if ((1 << i) & devmask) /* only display valid devices */
fprintf(stderr, "%s ", sound_device_names[i]);
fprintf(stderr, "\n");
exit(1);
}
int main(int argc, char *argv[])
{
int left, right, level; /* gain settings */
int status; /* return value from system calls */
int device; /* which mixer device to set */
int i; /* general purpose loop counter */
char *dev; /* mixer device name */
/* save program name */
name = argv[0];
/* open mixer, read only */
fd = open("/dev/mixer", O_RDONLY);
if (fd == -1) {
perror("unable to open /dev/mixer");
exit(1);
}
/* get needed information about the mixer */
status = ioctl(fd, SOUND_MIXER_READ_DEVMASK, &devmask);
if (status == -1)
perror("SOUND_MIXER_READ_DEVMASK ioctl failed");
status = ioctl(fd, SOUND_MIXER_READ_STEREODEVS, &stereodevs);
if (status == -1)
perror("SOUND_MIXER_READ_STEREODEVS ioctl failed");
/* check that user passed two or three arguments on command line */
if (argc != 3 && argc != 4)
usage();
/* save mixer device name */
dev = argv[1];
/* figure out which device to use */
for (i = 0 ; i < SOUND_MIXER_NRDEVICES ; i++)
if (((1 << i) & devmask) && !strcmp(dev, sound_device_names[i]))
break;
if (i == SOUND_MIXER_NRDEVICES) { /* didn't find a match */
fprintf(stderr, "%s is not a valid mixer device\n", dev);
usage();
}
/* we have a valid mixer device */
device = i;
/* get gain values */
if (argc == 4) {
/* both left and right values given */
left = atoi(argv[2]);
right = atoi(argv[3]);
} else {
/* left and right are the same */
left = atoi(argv[2]);
right = atoi(argv[2]);
}
/* display warning if left and right gains given for non-stereo device */
if ((left != right) && !((1 << i) & stereodevs)) {
fprintf(stderr, "warning: %s is not a stereo device\n", dev);
}
/* encode both channels into one value */
level = (right << 8) + left;
/* set gain */
status = ioctl(fd, MIXER_WRITE(device), &level);
if (status == -1) {
perror("MIXER_WRITE ioctl failed");
exit(1);
}
/* unpack left and right levels returned by sound driver */
left = level & 0xff;
right = (level & 0xff00) >> 8;
/* display actual gain setting */
fprintf(stderr, "%s gain set to %d%% / %d%%\n", dev, left, right);
/* close mixer device and exit */
close(fd);
return 0;
}
A typical use of the program to set the external CD input levels would look like:
% mixer cd 80 90 cd gain set to 80% / 90%
Going briefly through the program's source code, function main starts by opening the mixer and getting information about it that will be needed later. We get the user's first command-line argument, then loop through all of the valid mixer channels looking for a match to the given channel name. If there is no match then the command line is in error, and we quit.
Otherwise, we get either one or two gain parameters from the command line, convert them to integers, and encode them into the single number format used by the mixer functions. We then set the level using a mixer ioctl call. Finally, we unpack and display the actual gain values that were returned by the sound driver.
Note that all ioctl calls are checked for successful return codes. Another nicety is to check that when the user specifies different left and right gain values, the channel really supports stereo. If not, we warn the user. Another helpful feature is that the command usage line lists the valid mixer channel names:
% mixer
usage: mixer <device> <left-gain%> <right-gain%>
mixer <device> <gain%>
Where <device> is one of:
vol synth pcm line mic cd
Otherwise the user would have to guess or read the documentation to determine the valid mixer channel names. A useful enhancement to this program, to turn it into a full-featured mixer program, would be to add the ability to set the mixer recording source. I leave that as an exercise for you, the reader.
Programming /dev/sequencer
Programming /dev/audio