Basic Device Programming

Basic Device Programming

Typical usage for controlling a device under Linux consists of using a small set of function calls, usually called system calls, because they are implemented by the operating system’s kernel. The interface is virtually identical to the one used for regular disk files. This standardized method of accessing devices means programmers don’t need to learn new functions for each type of device. Programs can be written that will work with many different types of devices as well as disk files.

 

The open system call establishes access to the device, returning a file descriptor to be used for subsequent calls. The read and write functions receive data from and send data to a device, respectively. The ioctl routine is a catch-all function to perform other operations that do not fit the read/write model. For instance, to set mixer gains, the mixer driver offers an ioctl command that would be meaningless in any other devices.

Finally, close is used to notify the operating system that the device is no longer in use. The operating system normally closes all open devices when a program exits, but it is good practice to explicitly close them in your application. Most devices support a subset of these operations. For example, some devices may be read only, and not support the write function.

 

Some C programmers may not be familiar with the read and write functions. File and terminal access typically uses the buffered input/output routines such as printf and scanf. These are usually more efficient for files because the reads and writes are buffered in memory and performed later in larger blocks, reducing the overhead associated with calling the kernel read and write routines repeatedly. For low-level access to multimedia devices, you normally do not want this–you generally want data to be serviced immediately and have explicit control over buffer sizes. I will now look at each of these system calls in more detail.

 

The open System Call

This system call follows the format:

int open(const char *pathname, int flags, int mode);

 

This function is used to gain access to a device so you can subsequently operate on it with other system calls. The device or file can be an existing one that is to be opened for reading, writing, or both. It can also be used to create a new file.

The pathname parameter is the name of the file to be operated on. It can be a regular file, or a device file such as /dev/dsp. The flags parameter indicates the mode to be used for opening the file, and takes one of the following values:

 

 

O_RDONLY

open for read only

 

O_WRONLY

open for write only

 

O_RDWR

open for both read and write

In addition, some flags can be “bitwise OR” with the ones above to control other aspects of opening the file. A number of flags are defined, most of which are device-specific and not important to our discussion here.

The third mode parameter is optional–it specifies file permissions to be used when creating a new file and is only used when the O_CREAT option is given.

 

The open call, if successful, returns an integer file descriptor (a small positive number) to be used in subsequent system calls to reference the file. If the open fails for some reason, the call returns -1 and sets the variable errno to a value indicating the reason for failure.

There are some other more obscure options not relevant to our purposes; see the open(2) manpage for details.

The read System Call

The format of this function is:

int read(int fd, char *buf, size_t count);

 

This call returns data from a file or device. The first parameter is a file descriptor, obtained from a previous call to open. The buf parameter points to a buffer in which to hold the data returned–a sequence of bytes. The char * definition for the buffer is a convenience to cover all kinds of data. The argument is often cast to another data type, such as a data structure, that represents the particular kind of data you’re dealing with. The count parameter indicates the maximum number of bytes to be read. If successful, the function returns the actual number of bytes read, which is sometimes less than count. On error, the value -1 is returned and the global variable errno is set to a value indicating the error cause.

Calling read can cause a process to block until the data is available.

 

The write System Call

Writing data uses the write system call, which takes the form:

size_t write(int fd, const char *buf, size_t count);

 

This function is analogous to read, but sends data to a file or device. Parameter fd is the open file descriptor, buf points to the data to be written, and count indicates the number of bytes to be written. The function returns the number of bytes actually written, or -1 if an error occurred. Like the read call, the process may be blocked by the kernel until the data has been successfully written.

 

The ioctl System Call

The ioctl system call, a catch-all function, takes the form:

int ioctl(int fd, int request, ...);

 

This function is used for performing miscellaneous operations on a file or device that does not fit into the read or write calls. Each request may set some behavior of the device, return information, or both. It is device-specific.

The first parameter is a file descriptor, obtained when the device was opened. The second is an integer value indicating the type of ioctl request being made. There is usually a third parameter, which is dependent on the specific ioctl request being made.

Later in the chapter I will show some examples of using ioctl on multimedia sound devices.

 

The close System Call

The last of the basic functions follows this format:

int close(int fd);

 

The close system call notifies the kernel that access to a file or device is no longer required, allowing any related resources to be freed up.

As there is a limit on the number of files that any one process can have open at one time, it is good practice to close files or devices when you are finished with them.

 

Example Program

The simple program in Example 14-1 illustrates most of the concepts discussed so far.Example of Linux System Calls

/*
 * syscalls.c
 * Program to illustrate common system calls. Doesn't actually
 * perform any useful function, but will later be expanded into
 * a program which does.
 */

#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <linux/soundcard.h>

int main()
{
  int fd;	            /* device file descriptor */
  int arg;	            /* argument for ioctl call */
  unsigned char buf[1000];  /* buffer to hold data */
  int status;               /* return status of system calls */

  /* open device */
  status = fd = open("/dev/dsp", O_RDWR);
  if (status == -1) {
    perror("error opening /dev/dsp");
    exit(1);
  }

  /* set a parameter using ioctl call */
  arg = 8000; /* sampling rate */
  status = ioctl(fd, SOUND_PCM_WRITE_RATE, &arg);
  if (status == -1) {
    perror("error from SOUND_PCM_WRITE_RATE ioctl");
    exit(1);
  }

  /* read some data */
  status = read(fd, buf, sizeof(buf));
  if (status == -1) {
    perror("error reading from /dev/dsp");
    exit(1);
  }

  /* write some data */
  status = write(fd, buf, sizeof(buf));
  if (status == -1) {
    perror("error writing to /dev/dsp");
    exit(1);
  }

  /* close the device */
  status = close(fd);
  if (status == -1) {
    perror("error closing /dev/dsp");
    exit(1);
  }

  /* and exit */
  return(0);
}

First I include the header files that define the library routines used in the program. An easy way to identify these is to read the relevant manpages for the functions. I then start the function main, the only one in this small program, and define the variables needed to hold the file descriptor, the argument to ioctl, the data buffer, and the status returned by the system calls used.

 

I open the device file /dev/dsp, indicating to open for both read and write. The third parameter is not needed as I am not creating a new file. After calling open, I check the return value, which displays an error message and exits if the call was not successful. I then use the ioctl call to set a parameter of the device. The meaning and type of the argument is specific to this ioctl function, but can be ignored for now. I’ll cover it later.

Next I call read to obtain some data bytes from the device. I again check the return status. Then the same data is written back in a similar manner using the write system call.

 

The last step is to close the device, and again I check the status of the call to close, although some programmers might consider this level of checking a bit paranoid.

 

If you are new to C programming under Linux, I recommend that you enter the example program on your system and run it. Don’t worry yet about what it does, just concentrate on successfully compiling it and verifying that it runs without errors. Try changing the program so that it attempts to operate on a nonexistent device and check that an error message is produced. Can you think of ways to produce any other error messages from the sample program?

 

Sound Programming Basics

The Linux sound driver tries to present an idealized sound card interface to the application programmer. It takes care of the hardware differences between cards although you do sometimes have to worry about whether a specific function is supported or not (e.g., whether a MIDI interface exists).

 

The various hardware blocks (D/A converter, synthesizer, etc.) are controlled by the CPU via the ISA bus. The ISA bus consists of data, address, and control signals that connect to the sound card via the slot in which the sound card is inserted. The bus allows the CPU to control the devices on the sound card, but it has no support for analog inputs or outputs; these are typically supported by connectors on the rear panel of the card.

[1] Sound cards could use buses other than ISA, such as EISA and PCI, but these are not in common use.

 

There are several analog inputs on the left-hand side–microphone, CD audio, and line level input are the most common. On the right side are the analog outputs–usually speaker and line level as a minimum. An output mixer combines the various signal sources and sends them to the output devices. The signal sources may include input devices, to support playing the input signals through the speakers (a common example is playing audio CDs through the sound card using a CD-ROM drive).

 

Similarly, an input mixer accepts several analog signals, combines them, and feeds the output to an analog to digital converter to be digitized. Many sound cards also provide CD-ROM, MIDI, and joystick interfaces.

 

Applications that access the sound devices should include the sound header file linux/soundcard.h. Some additional functions specific to the Gravis UltraSound card are defined in linux/ultrasound.h.In the next sections I will step through each of the sound driver device files, exploring the application programming interface, and looking at some small code examples. I recommend trying the code out on your system and modifying it, to fully illustrate the concepts.

 


 

Programming /dev/sndstat

The device /dev/sndstat/ is the simplest device provided by the sound driver. A read-only device file, its only purpose is to report information about the sound driver in human-readable form. It is much like the files found in the /proc filesystem (and arguably should be found there).

The device is really meant for human use; it will probably never be read from a program, as you can get the same information more easily using ioctl calls. It is useful for checking your hardware configuration (DMA channel, IRQ number, etc.) and for finding out the version of the sound driver being used.

 

Included in the output are some “magic numbers” you can pass to a kernel loader program such as LILO to configure the sound driver at run-time.

Here is a sample output, produced using the shell cat command:

% cat /dev/sndstat
Sound Driver:3.5.2-960330 (Tue May 21 19:08:17 EDT 1996 root,
Linux fizzbin 2.0.0 #1 Tue May 21 19:22:57 EDT 1996 i386
Kernel: Linux fizzbin 2.0.0 #1 Tue May 21 19:22:57 EDT 1996 i386
Config options: a80002
Installed drivers: 
Type 1: OPL-2/OPL-3 FM
Type 2: SoundBlaster
Type 6: SoundBlaster 16bit
Type 7: SB MPU
Card config: 
SoundBlaster at 0x220 irq 5 drq 1,5
(SB MPU at 0x0 irq 1 drq 0)
OPL-2/OPL-3 FM at 0x388 irq 0 drq 0
Audio devices:
0: SoundBlaster Pro 3.2
Synth devices:
0: Yamaha OPL-3
Midi devices:
0: SoundBlaster
Timers:
0: System clock
Mixers:
0: SoundBlaster
The shell command above is the usual way of reading this device.
I won't write any code that uses it, and in fact I recommend against developing any software that relies on the format of the output.
In the future, it is likely to change as the sound driver is enhanced.
Previous Next 

 

Programming /dev/dsp

/dev/dsp is the digital sampling and digital recording device, and probably the most important for multimedia applications. Writing to the device accesses the D/A converter to produce sound. Reading the device activates the A/D converter for sound recording and analysis.

The name DSP comes from the term digital signal processor, a specialized processor chip optimized for digital signal analysis. Sound cards may use a dedicated DSP chip, or may implement the functions with a number of discrete devices. Other terms that may be used for this device are digitized voice and PCM.

Some sounds cards provide more than one digital sampling device; in this case a second device is available as /dev/dsp1. Unless noted otherwise, this device operates in the same manner as /dev/dsp.

The DSP device is really two devices in one. Opening for read-only access allows you to use the A/D converter for sound input. Opening for write only will access the D/A converter for sound output. Generally speaking you should open the device either for read only or for write only. It is possible to perform both read and write on the device, albeit with some restrictions; this will be covered in a later section.

Only one process can have the DSP device open at a time. Attempts by another process to open it will fail with an error code of EBUSY.

Reading from the DSP device returns digital sound samples obtained from the A/D converter. It is important to understand that the sampling rate is dependent on the kernel driver, and not the speed at which the application program reads it.

 

When reading from /dev/dsp you will never encounter an end-of-file condition. If data is read too slowly (less than the sampling rate), the excess data will be discarded, resulting in gaps in the digitized sound. If you read the device too quickly, the kernel sound driver will block your process until the required amount of data is available.

The input source depends on the mixer setting (which I will look at shortly); the default is the microphone input. The format of the digitized data depends on which ioctl calls have been used to set up the device. Each time the device is opened, its parameters are set to default values. The default is 8-bit unsigned samples, using one channel (mono), and an 8 kHz sampling rate.

Writing a sequence of digital sample values to the DSP device produces sound output. This process is illustrated in Figure 14-2(b). Again, the format can be defined using ioctl calls, but defaults to the values given above for the read system call (8-bit unsigned data, mono, 8 kHz sampling).

If the data are written too slowly, there will be dropouts or pauses in the sound output. Writing the data faster than the sampling rate will simply cause the kernel sound driver to block the calling process until the sound card hardware is ready to process the new data. Unlike some devices, there is no support for non-blocking I/O.

If you don’t like the defaults, you can change them through ioctl calls. In general you should set the parameters after opening the device, and before any calls to read or write. You should also set the parameters in the order in which they are described below.

All DSP ioctl calls take a third argument that is a pointer to an integer. Don’t try to pass a constant; you must use a variable. The call will return -1 if an error occurs, and set the global variable errno.

If the hardware doesn’t support the exact value you call for, the sound driver will try to set the parameter to the closest allowable value. For example, with my sound card, selecting a sampling rate of 9000 Hz will result in an actual rate of 9009 Hz being used.

If a parameter is out of range, the driver will set it to the closest value (i.e., the upper or lower limit). For example, attempting to use 16-bit sampling with an 8-bit sound card will result in the driver selecting 8 bits, but no error will be returned. It is up to you, the programmer, to verify that the value returned is acceptable to your application.

All of the ioctl calls for the DSP device are names starting with SOUND_PCM. Calls in the form SOUND_PCM_READ_XXX are used to return just the current value of a parameter. To change the values, the ioctl calls are named like SOUND_PCM_WRITE_XXX. As discussed above, these calls also return the selected value, which is not necessarily the same as the value passed to the sound driver.

The ioctl constants are defined in the header file linux/soundcard.h. Let’s examine each of them in detail.

 

SOUND_PCM_WRITE_BITS

Sets the sample size, in bits. Valid choices are 8 and 16, but some cards do not support 16.

 

SOUND_PCM_READ_BITS

Returns the current sample size, which should be either 8 or 16 bits.

 

SOUND_PCM_WRITE_CHANNELS

Sets the number of channels–1 for mono, 2 for stereo. When running in stereo mode, the data is interleaved when read or written, in the format left-right-left-right…. Remember that some sound cards do not support stereo; check the actual number of channels returned in the argument.

 

SOUND_PCM_READ_CHANNELS

Returns the current number of channels, either 1 or 2.

 

SOUND_PCM_WRITE_RATE

Sets the sampling rate in samples per second. Remember that all sound cards have a limit on the range; the driver will round the rate to the nearest speed supported by the hardware, returning the actual (rounded) rate in the argument. Typical lower limits are 4 kHz; upper limits are 13, 15, 22, or 44 kHz.

 

SOUND_PCM_READ_RATE

Returns just the current sampling rate. This is the rate used by the kernel, which may not be exactly the rate given in a previous call to SOUND_PCM_WRITE_RATE, because of the previously discussed rounding.

Sample Program

I will now illustrate programming of the DSP device with a short example. I call the program in Example 14-2 parrot. It records a few seconds of audio, saving it to an array in memory, then plays it back.Reading and Writing the /dev/dsp Device

/*
 * parrot.c
 * Program to illustrate /dev/dsp device
 * Records several seconds of sound, then echoes it back.
 * Runs until Control-C is pressed.
 */

#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <stdio.h>
#include <linux/soundcard.h>

#define LENGTH 3    /* how many seconds of speech to store */
#define RATE 8000   /* the sampling rate */
#define SIZE 8      /* sample size: 8 or 16 bits */
#define CHANNELS 1  /* 1 = mono 2 = stereo */

/* this buffer holds the digitized audio */
unsigned char buf[LENGTH*RATE*SIZE*CHANNELS/8];

int main()
{
  int fd;	/* sound device file descriptor */
  int arg;	/* argument for ioctl calls */
  int status;   /* return status of system calls */

  /* open sound device */
  fd = open("/dev/dsp", O_RDWR);
  if (fd < 0) {
    perror("open of /dev/dsp failed");
    exit(1);
  }

  /* set sampling parameters */
  arg = SIZE;	   /* sample size */
  status = ioctl(fd, SOUND_PCM_WRITE_BITS, &arg);
  if (status == -1)
    perror("SOUND_PCM_WRITE_BITS ioctl failed");
  if (arg != SIZE)
    perror("unable to set sample size");

  arg = CHANNELS;  /* mono or stereo */
  status = ioctl(fd, SOUND_PCM_WRITE_CHANNELS, &arg);
  if (status == -1)
    perror("SOUND_PCM_WRITE_CHANNELS ioctl failed");
  if (arg != CHANNELS)
    perror("unable to set number of channels");

  arg = RATE;	   /* sampling rate */
  status = ioctl(fd, SOUND_PCM_WRITE_RATE, &arg);
  if (status == -1)
    perror("SOUND_PCM_WRITE_WRITE ioctl failed");

  while (1) { /* loop until Control-C */
    printf("Say something:n");
    status = read(fd, buf, sizeof(buf)); /* record some sound */
    if (status != sizeof(buf))
      perror("read wrong number of bytes");
    printf("You said:n");
    status = write(fd, buf, sizeof(buf)); /* play it back */
    if (status != sizeof(buf))
      perror("wrote wrong number of bytes");
    /* wait for playback to complete before recording again */
    status = ioctl(fd, SOUND_PCM_SYNC, 0); 
  if (status == -1)
    perror("SOUND_PCM_SYNC ioctl failed");
  }
}

The source file starts by including a number of standard header files, including linux/soundcard.h. Then some constants are defined for the sound card settings used in the program, which makes it easy to change the values used. A static buffer is defined to hold the sound data.

I first open the DSP device for both read and write and check that the open was successful. Next I set the sampling parameters using ioctl calls. Notice that a variable must be used because the driver expects a pointer. In each case I check for an error from the ioctl call (a return value of -1), and that the values actually used are within range. This programming may appear to be overly cautious, but I consider it good coding practice that pays off when trying to debug the code. Note that I do not check that the actual sampling rate returned matches the selected rate because of the sampling rate rounding previously described.

I then run in a loop, first prompting the user to speak, then reading the sound data into the buffer. Once the data is received, I warn the user, then write the same data back to the DSP device, where it should be heard. This repeats until the program is interrupted with Control-C.

The SOUND_PCM_SYNC ioctl has not yet been mentioned. I’ll show what this is used for in the section titled “Advanced Sound Programming,” later in this chapter.

Try compiling and running this program. Then make some enhancements:

 

 

  1. Make the parameters selectable using command-line options (sample rate, size, time). See the effect on sound quality with different sampling rates.

     

  2. Reverse the sound samples (and listen for hidden messages), or play them back at a different sampling rate from the one at which they were recorded.

     

  3. Automatically start recording when the voice starts and stop when silence occurs (or a maximum time is reached). Hints: for 8-bit unsigned data the zero value is 0x80, but you will likely see values that vary around this level due to noise. Set a noise threshold (or better yet, measure the background noise level at the start of the program).

     

  4. Bonus question: modify the program so that it can recognize the words that are spoken.

     

 

 


Programming /dev/audio

The /dev/audio device is similar to /dev/dsp, but is intended for compatibility with the audio device on workstations made by Sun Microsystems, where it has the same name. The device uses mu-law encoding. It does not support the SunOS ioctl functions (in principle it could, though). The main purpose is to support user commands such as cat file.au >/dev/audio to play Sun mu-law encoded files.

It is not recommended that you use /dev/audio for new application programs. The mu-law encoding adds software overhead and some distortion. It is better to use the /dev/dsp interface instead. Only one of /dev/audio and /dev/dsp is available at any one time, as they are just different software interfaces to the same hardware device. There is also a /dev/audio1 device available for those cards that have a second sampling device.

 

 


 

 

Programming /dev/mixer

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.

Example Program

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:nn"
	 "Mixer      Device     Recording  Active     Stereo     Currentn"
	 "Channel    Available  Source     Source     Device     Leveln"
	 "---------  ---------  ---------  --------   ---------  ---------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%%>nn"
	  "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 devicen", 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 devicen", 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

The dev/sequencer device allows you to program the FM or wavetable synthesizer built into the sound card or external devices on a MIDI bus. It is intended for computer music applications.

The word sequencer refers to an application that controls MIDI devices, sending messages to them in an appropriate sequence to play music.

The interface to this device is quite complex. It uses commands loosely based on the events used by the MIDI protocol. A typical command might start playing a note using a particular instrument voice.

Before using the on-board synthesizer, you have to download patches to define one or more instruments. These can be individual patches loaded on a per-instrument basis or, more commonly, a full set such as the 128 standard MIDI voices. These patches are usually contained in files.

I’m not going to cover this device in any more detail. It is of less interest to most multimedia application developers because it is limited to music, and applications already exist that can play most computer music files (these were covered in Part III).

There is a higher-level interface to the synthesizer and MIDI bus, /dev/sequencer2, but it is not yet available in the current sound driver. A higher-level interface for managing patches for the /dev/sequencer devices is planned, but not yet complete. Called /dev/patmgr, it is quite specialized and probably not of interest to most multimedia application developers, so I won’t explore it further.

The device /dev/midi will provide low-level access to the MIDI bus. It is not currently fully implemented, and the API is in the process of changing. The MIDI interface is somewhat specialized and normally used only by musicians.

If you are interested in computer music, I urge you to look at the source code for some of the existing software applications such as mp02, adagio, and glib, in order to gain an understanding of the programming interface.

Example Program

For what it’s worth, in Example 14-5 I offer a small example program that illustrates how to determine the sequencer devices supported by a sound card.Example of Accessing /dev/sequencer

/*
 * seq_info.c
 * Example program for /dev/sequencer
 */
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>

#include <sys/soundcard.h>

/* return string given a MIDI device type */
char *midi_device_type(int type)
{
  switch (type) {
  case SNDCARD_ADLIB:      return("Adlib");
  case SNDCARD_SB:         return("SoundBlaster");
  case SNDCARD_PAS:        return("ProAudioSpectrum");
  case SNDCARD_GUS:        return("GravisUltraSound");
  case SNDCARD_MPU401:     return("MPU401");
  case SNDCARD_SB16:       return("SoundBlaster16");
  case SNDCARD_SB16MIDI:   return("SoundBlaster16 MIDI");
  case SNDCARD_UART6850:   return("6850 UART");
  case SNDCARD_GUS16:      return("Gravis UltraSound 16");
  case SNDCARD_MSS:        return("Microsoft Sound System");
  case SNDCARD_PSS:        return("Personal Sound System");
  case SNDCARD_SSCAPE:     return("Ensoniq SoundScape");
/* these require a more recent version of the sound driver */
#if SOUND_VERSION >= 301
  case SNDCARD_PSS_MPU:    return("Personal Sound System + MPU");
  case SNDCARD_PSS_MSS:    return("Personal Sound System/Microsoft Sound 
                                   System");
  case SNDCARD_TRXPRO_MPU: return("MediaTrix Pro + MPU");
  case SNDCARD_MAD16:      return("MAD16");
  case SNDCARD_MAD16_MPU:  return("MAD16 + MPU");
  case SNDCARD_CS4232:     return("CS4232");
  case SNDCARD_CS4232_MPU: return("CS4232 + MPU");
  case SNDCARD_MAUI:       return("Maui");
  case SNDCARD_PSEUDO_MSS: return("Pseudo-MSS");
#endif /* SOUND_VERSION >= 301 */
#if SOUND_VERSION >= 350
  case SNDCARD_GUSPNP:     return ("Gravis UltraSound PlugNPlay");
#endif /* SOUND_VERSION >= 301 */
  default:                 return("unknown");
  }
}
/* return string given a synthesizer device type */
char *synth_device_type(int type)
{
  switch (type) {
  case SYNTH_TYPE_FM:     return("FM");
  case SYNTH_TYPE_SAMPLE: return("Sampling");
  case SYNTH_TYPE_MIDI:   return("MIDI");
  default:                return("unknown");
  }
}

/* return string given a synthesizer device subtype */
char *synth_device_subtype(int type)
{
  switch (type) {
  case FM_TYPE_ADLIB:   return("Adlib");
  case FM_TYPE_OPL3:    return("OPL-3");
  case SAMPLE_TYPE_GUS: return("GUS");
  default:              return("unknown");
  }
}

int main(int argc, char *argv[])
{
  int i, status, numdevs;
  struct synth_info sinfo;
  struct midi_info minfo;
  int fd;        /* file descriptor for /dev/sequencer */
  char *device; /* name of device to report on */

  /* get device name from command line or use default */  
  if (argc == 2)
    device = argv[1];
  else
    device = "/dev/sequencer";

  /* try to open device */
  fd = open(device, O_WRONLY);
  if (fd == -1) {
    fprintf(stderr, "%s: unable to open `%s', ", argv[0], device);
    perror("");
    return 1;
  }

  status = ioctl(fd, SNDCTL_SEQ_NRSYNTHS, &numdevs);
  if (status == -1) {
    perror("ioctl failed");
    exit(1);
  }
  printf("%s:n%d synthesizer device%s installedn", device, numdevs,
	 numdevs == 1 ? "" : "s");

  for (i = 0 ; i < numdevs ; i++) {
    sinfo.device = i;
    status = ioctl(fd, SNDCTL_SYNTH_INFO, &sinfo);
    if (status == -1) {
      perror("ioctl failed");
      exit(1);
    }
    printf("Device %d: `%s' type: `%s' subtype: `%s' voices: %dn",
	   i,
	   sinfo.name,
	   synth_device_type(sinfo.synth_type),
	   synth_device_subtype(sinfo.synth_subtype),
	   sinfo.nr_voices);
  }
  status = ioctl(fd, SNDCTL_SEQ_NRMIDIS, &numdevs);
  if (status == -1) {
    perror("ioctl failed");
    exit(1);
  }
  printf("%d MIDI device%s installedn", numdevs,
	 numdevs == 1 ? "" : "s");
  for (i = 0 ; i < numdevs ; i++) {
    minfo.device = i;
    status = ioctl(fd, SNDCTL_MIDI_INFO, &minfo);
    if (status == -1) {
      perror("ioctl failed");
      exit(1);
    }
    printf("Device %d: `%s' type: `%s'n",
	   i,
	   minfo.name,
	   midi_device_type(minfo.dev_type));
  }
  /* close file and exit */
  close(fd);
  return 0;
}

On my system, the program reported one FM synthesizer present and one MIDI interface, and produced the following output:

/dev/sequencer:
1 synthesizer device installed
Device 0: `Yamaha OPL-3' type: `FM' subtype: `OPL-3' voices: 18
1 MIDI device installed
Device 0: `SoundBlaster' type: `SoundBlaster'

 

 

Advanced Sound Programming

This section describes some miscellaneous sound programming issues that require special consideration or are less commonly used.

We saw earlier that /dev/dsp operates using unsigned data, either 8 or 16 bits in size, while /dev/audio uses mu-law encoded data. It is possible to change the data formats a device uses with the SOUND_PCM_SETFMT ioctl call. A number of data formats are defined in the soundcard.h header file, all prefixed with the string AFMT_. For example, to set the coding format to mu-law, you could use:

fmt = AFMT_MU_LAW;
ioctl(fd, SOUND_PCM_SETFMT, &fmt);

The argument will be returned with the coding format that was selected by the kernel (which will be the same as the one selected unless the device does not support it). The special format AFMT_QUERY will return default format for the device. To find out all of the formats that a given device supports, you can use the SOUND_PCM_GETFMTS ioctl. It returns a bitmask that has bits set for each of the supported formats.

The SNDCTL_DSP_GETBLKSIZE ioctl returns the block size that the sound driver uses for data transfers. The returned value is an integer, indicating the number in bytes. This information can be useful in an application program for selecting a buffer size that ensures that the data passed to the driver is transferred in complete blocks.

The SNDCTL_DSP_GETCAPS ioctl returns a bitmask identifying various capabilities of a sound card DSP device. They are listed in soundcard.h with labels prefixed by DSP_CAP. A typical capability is DSP_CAP_DUPLEX, a boolean flag indicating whether the device supports full duplex mode (simultaneous record and playback).

Example 14-6 illustrates these system calls, displaying information about a DSP device (/dev/dsp by default).Determining DSP Capabilities

/*
 * dsp_info.c
 * Example program to display sound device capabilities
 */

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <linux/soundcard.h>

/* utility function for displaying boolean status */
static char *yes_no(int condition)
{
  if (condition) return "yes"; else return "no";
}

/*
 * Set sound device parameters to given values. Return -1 if
 * values not valid. Sampling rate is returned.
 */
static int set_dsp_params(int fd, int channels, int bits, int *rate) {
  int status, val = channels;

  status = ioctl(fd, SOUND_PCM_WRITE_CHANNELS, &val);
  if (status == -1)
    perror("SOUND_PCM_WRITE_CHANNELS ioctl failed");
  if (val != channels) /* not valid, so return */
    return -1;
  val = bits;
  status = ioctl(fd, SOUND_PCM_WRITE_BITS, &val);
  if (status == -1)
    perror("SOUND_PCM_WRITE_BITS ioctl failed");
  if (val != bits)
    return -1;
  status = ioctl(fd, SOUND_PCM_WRITE_RATE, rate);
  if (status == -1)
    perror("SOUND_PCM_WRITE_RATE ioctl failed");
  return 0;
}

int main(int argc, char *argv[])
{
  int rate;
  int channels;            /* number of channels */
  int bits;                /* sample size */
  int blocksize;           /* block size */
  int formats;             /* data formats */
  int caps;                /* capabilities */
  int deffmt;              /* default format */
  int min_rate, max_rate;  /* min and max sampling rates */
  char *device;            /* name of device to report on */
  int fd;                  /* file descriptor for device */
  int status;              /* return value from ioctl */

  /* get device name from command line or use default */  
  if (argc == 2)
    device = argv[1];
  else
    device = "/dev/dsp";

  /* try to open device */
  fd = open(device, O_RDWR);
  if (fd == -1) {
    fprintf(stderr, "%s: unable to open `%s', ", argv[0], device);
    perror("");
    return 1;
  }
  
  status = ioctl(fd, SOUND_PCM_READ_RATE, &rate);
  if (status ==  -1)
    perror("SOUND_PCM_READ_RATE ioctl failed");
  status = ioctl(fd, SOUND_PCM_READ_CHANNELS, &channels);
  if (status ==  -1)
    perror("SOUND_PCM_READ_CHANNELS ioctl failed");
  status = ioctl(fd, SOUND_PCM_READ_BITS, &bits);
  if (status ==  -1)
    perror("SOUND_PCM_READ_BITS ioctl failed");
  status = ioctl(fd, SNDCTL_DSP_GETBLKSIZE, &blocksize);
  if (status ==  -1)
    perror("SNFCTL_DSP_GETBLKSIZE ioctl failed");
  
  printf(
	 "Information on %s:nn"
	 "Defaults:n"
	 "  sampling rate: %d Hzn"
	 "  channels: %dn"
	 "  sample size: %d bitsn"
	 "  block size: %d bytesn",
	 device, rate, channels, bits, blocksize
	 );

/* this requires a more recent version of the sound driver */
#if SOUND_VERSION >= 301
  printf("nSupported Formats:n");
  deffmt = AFMT_QUERY;
  status = ioctl(fd, SOUND_PCM_SETFMT, &deffmt);
  if (status ==  -1)
    perror("SOUND_PCM_SETFMT ioctl failed");
  status = ioctl(fd, SOUND_PCM_GETFMTS, &formats);
  if (status ==  -1)
    perror("SOUND_PCM_GETFMTS ioctl failed");
  if (formats & AFMT_MU_LAW) {
    printf("  mu-law");
    (deffmt == AFMT_MU_LAW) ? printf(" (default)n") : printf("n");
  }
  if (formats & AFMT_A_LAW) {
    printf("  A-law");
    (deffmt == AFMT_A_LAW) ? printf(" (default)n") : printf("n");
  }
  if (formats & AFMT_IMA_ADPCM) {
    printf("  IMA ADPCM");
    (deffmt == AFMT_IMA_ADPCM) ? printf(" (default)n") : printf("n");
  }
  if (formats & AFMT_U8) {
    printf("  unsigned 8-bit");
    (deffmt == AFMT_U8) ? printf(" (default)n") : printf("n");
  }
  if (formats & AFMT_S16_LE) {
    printf("  signed 16-bit little-endian");
    (deffmt == AFMT_S16_LE) ? printf(" (default)n") : printf("n");
  }
  if (formats & AFMT_S16_BE) {
    printf("  signed 16-bit big-endian");
    (deffmt == AFMT_S16_BE) ? printf(" (default)n") : printf("n");
  }
  if (formats & AFMT_S8) {
    printf("  signed 8-bit");
    (deffmt == AFMT_S8) ? printf(" (default)n") : printf("n");
  }
  if (formats & AFMT_U16_LE) {
    printf("  unsigned 16-bit little-endian");
    (deffmt == AFMT_U16_LE) ? printf(" (default)n") : printf("n");
  }
  if (formats & AFMT_U16_BE) {
    printf("  unsigned 16-bit big-endian");
    (deffmt == AFMT_U16_BE) ? printf(" (default)n") : printf("n");
  }
  if (formats & AFMT_MPEG) {
    printf("  MPEG 2");
    (deffmt == AFMT_MPEG) ? printf(" (default)n") : printf("n");
  }
  
  printf("nCapabilities:n");
  status = ioctl(fd, SNDCTL_DSP_GETCAPS, &caps);
  if (status ==  -1)
    perror("SNDCTL_DSP_GETCAPS ioctl failed");
  printf(
	 "  revision: %dn"
	 "  full duplex: %sn"
	 "  real-time: %sn"
	 "  batch: %sn"
	 "  coprocessor: %sn" 
	 "  trigger: %sn"
	 "  mmap: %sn",
	 caps & DSP_CAP_REVISION,
	 yes_no(caps & DSP_CAP_DUPLEX),
	 yes_no(caps & DSP_CAP_REALTIME),
	 yes_no(caps & DSP_CAP_BATCH),
	 yes_no(caps & DSP_CAP_COPROC),
	 yes_no(caps & DSP_CAP_TRIGGER),
	 yes_no(caps & DSP_CAP_MMAP));

#endif /* SOUND_VERSION >= 301 */
  
  /* display table heading */
  printf(
	 "nModes and Limits:n"
	 "Device    Sample    Minimum   Maximumn"
	 "Channels  Size      Rate      Raten"
	 "--------  --------  --------  --------n"
	 );
  
  /* do mono and stereo */  
  for (channels = 1; channels <= 2 ; channels++) {
    /* do 8 and 16 bits */
    for (bits = 8; bits <= 16 ; bits += 8) {
      /* To find the minimum and maximum sampling rates we rely on
	 the fact that the kernel sound driver will round them to
	 the closest legal value. */
      min_rate = 1;
      if (set_dsp_params(fd, channels, bits, &min_rate) == -1)
	continue;
      max_rate = 100000;
      if (set_dsp_params(fd, channels, bits, &max_rate) == -1)
	continue;
      /* display the results */
      printf("%8d  %8d  %8d  %8dn", channels, bits, min_rate, max_rate);
    }
  }
  close(fd);
  return 0;
}

Typical output from the dsp_info program looks like this:

Information on /dev/dsp:
Defaults:
  sampling rate: 8000 Hz
  channels: 1
  sample size: 8 bits
  block size: 4096 bytes
Supported Formats:
  mu-law
  unsigned 8-bit (default)
Capabilities:
  revision: 1
  full duplex: no
  real-time: no
  batch: no
  coprocessor: no
  trigger: yes
  mmap: yes
Modes and Limits:
Device    Sample    Minimum   Maximum
Channels  Size      Rate      Rate
--------  --------  --------  --------
       1         8      4000     43478
       2         8      4000     21739

I mentioned earlier that you can’t record and play back at the same time with one sound device. You can, however, change parameters such as sampling rate and sample size “on the fly.” First, you need to open the PCM device for read and write. Then, before changing any parameters, use the ioctl call

ioctl(fd, SOUND_PCM_SYNC, 0);

in order to inform the sound driver that it should complete any data transfers that are in progress. You can now change parameters, or even switch between recording and playback. I used this feature earlier in the parrot example program.

You can also stop record or playback immediately using

ioctl(fd, SOUND_PCM_RESET, 0).

Unfortunately, a true bidirectional mode that allows simultaneous recording and playback is not supported (it likely will be in the future, though). This mode would be useful, for example, for implementing a computerized telephone utility that allows users to communicate using a sound card. There is one other alternative: some sound cards, such as the ProAudioSpectrum, have two independent PCM devices–/dev/dsp and /dev/dsp1. You can use one for read and one for write, resulting in simultaneous recording and playback. In order to perform the simultaneous data transfers, it would probably be best to implement the system as two separate processes.

Some applications are time critical. The sound driver transfers data using DMA buffers, a typical buffer size being 64 kilobytes. This can impact real-time applications because of the time needed to fill up buffers for transfer. Transferring 64K of data at 8 kHz would take eight seconds. If a multimedia application was performing an animation, for example, it would be unacceptable to have the display stop for eight seconds while the process was waiting for a full buffer of sound data. You can reduce the buffer size using the ioctl call in this form:

ioctl(fd, SOUND_PCM_SUBDIVIDE, &divisor);

The divisor parameter takes the value 1, 2, or 4; it reduces the DMA buffer size by the corresponding factor. Note that the divisor operates on the default buffer size, not the current value, so you cannot call the function repeatedly to keep reducing the divisor.

For some applications, the smaller DMA buffer size may still not be enough. When the program DOOM was ported to Linux, the performance of the game was impacted by the pauses required to play sound effects. A new real-time ioctl was added to address applications such as this one. The ioctl call is called SNDCTL_DSP_SETFRAGMENT, and is explained in the file experimental.txt included in the kernel sound driver source.

 


 

 

 

The PC Speaker Sound Driver

Chapter 8, The CD-ROM Driver, covered the installation and testing of the sound driver for the built-in PC speaker. Currently this driver is distributed as a kernel patch. This driver doesn’t require a sound card, only the standard PC speaker. It tries to be as compatible as possible with the real sound driver, supporting a subset of the kernel sound API. It also supports some homebrew sound hardware you can build yourself.

Not surprisingly it does not support emulation of an FM synthesizer or MIDI interface. It does support replacements for the /dev/audio and /dev/dsp devices, but for sound output only. There is also limited /dev/mixer support.

The driver uses unique device files and can co-exist with the real sound driver. Users that do not have a sound card typically create the standard sound device files as symbolic links to the PC speaker devices, so that sound applications will be able to open the expected devices. The driver includes a soundcard.h header file, which is necessary only if the user does not already have the standard sound header file installed. I’ll briefly look at all of the supported devices.

 

/dev/pcsp

This device emulates the /dev/dsp device, for output only. Attempts to read from it will always return the error EINVAL. It accepts all of the standard /dev/dsp ioctl calls (although some are simply ignored), including the mixer ioctl functions. Some additional ioctl calls exist for setting parameters specific to the PC speaker driver. You’ll find it easier to use the pcsel program (included with the driver source distribution) to set these parameters.

 

/dev/pcsp16

This device simulates a 16-bit sound card. The driver is optional, selected when configuring the kernel driver. Internally the driver is 8-bit, but it is included for applications that insist on a 16-bit sound device (most notably, the game DOOM).

 

/dev/pcaudio

This device is the analog of /dev/audio, the mu-law device. It has the same limitations as /dev/pcsp.

 

/dev/pcmixer

This device accepts all of the kernel mixer ioctl calls. Only a master volume control is supported, and there are no recording source devices. There is support for stereo if a stereo DAC device is used.

You can run the mixer_info and dsp_info programs listed earlier to display the devices’ capabilities.

The PC speaker driver is quite an accomplishment, considering the limitations of the hardware it has to work with. Many applications designed for the real sound card will work with the driver without changes. Results vary depending on the type of speaker installed and the speed of the system, but given a fast enough machine, you can even play MOD files.

Comments