Driving One's Own Audio Device

In this article Alessandro will show the design and implementation of a custom audio device, paying particular attention to the software driver. The driver, as usual, is developed as a kernel module. Even though Linux 2.2 will be out by the time you read this, the software described here works only with Linux 2.0 and the first few decades of 2.1 versions.
The Interrupt Handler

In sad, audio samples are strobed out by a hardware interrupt, which is reported to the processor every 125 microseconds. Each interrupt gets services by an ISR (interrupt service routine, also called “interrupt handler”), written in C. I won't go into the details of registering interrupt handlers here, as they have already been described in other “Kernel Korner” columns.

Managing several thousand interrupts per second is a non-negligible load for the processor (at least for slow processors like mine), so the driver only enables interrupt reporting when the device is opened and disables it on the last close.

What I'd like to show here is how data flows to the A/D converter. The code is quite easy, and the OBUFFER_THRESHOLD constant appears again, as expected:

if (!OBUFFER_EMPTY) { /* send a sample */
   OUTBYTE(*((u8 *)otail++));
   if (otail == obuffer + OBUFFER_SIZE)
      otail = obuffer; /* wrap */

As usual, every code snippet introduces new questions; this time you might wonder about OUTBYTE and closeq. The latter item is the main topic of the next section, while OUTBYTE hides the line of code that pushes a data sample to the D/A converter.

The macro is defined earlier in sad.c as follows:

#define OUTBYTE(b) outb(convert(b), sad_base)

Here, sad_base is the processor port used to send data to the parallel interface (usually 0x378), and convert is a simple mathematical conversion that turns the data byte as stored in the audio-file format to a linear 0-255 value, more suited to the D/A converter.

Blocking Close

The close system call, like read and write, is one of those calls that can block. For example, when you are done with the floppy drive, close blocks waiting for any data to be flushed to the physical device. This behaviour can be verified by running:

strace cp /boot/vmlinux /dev/fd0

Audio devices are somewhat similar to the floppy drive: a program writing audio data closes the file after the last write system call. However, this means only that data has been transferred to the output buffer, not that everything has necessarily already flown to the loudspeaker. An implementation that blocks on close can be helpful, when you want to do this:

cat file.au > /dev/sad && echo done
On the other hand, sometimes you'll prefer to stop playing sounds when the process closes the device. For example, if you play the piano on your keyboard, the sound should stop as soon as you raise the key, even if the program has already pushed extra data to the output buffer.

For this reason, the sad module implements two device entry points, one that blocks on close and one that doesn't block. Minor number 0 is the blocking device and minor number 1 is the non-blocking one. The entry points in /dev are created by the script that loads the module, included in the sad distribution: /dev/sad is the one that blocks on close and /dev/sadnb is the non-blocking one.

While real device drivers often offer configuration options (such as choosing whether or not to block on close) through the ioctl system call, I chose to offer different entry points in /dev, because this way I can use normal shell redirection to perform my tasks, without the need to write C code to perform the relevant ioctl call. The close method in sad.c, therefore, looks like the following:

if (MINOR(inode->i_rdev)==0) /* wait */
else {
   unsigned long flags; /* drop data */
   cli(); ohead=otail;
if (!MOD_IN_USE)
   SAD_IRQOFF(); /* disable irq */

Actually, there is a third possibility as far as close is concerned: go on playing in the background as long as some data is there, even after the program has closed the audio device. This approach is left as an exercise to the reader, because I prefer having a chance to actively stop any device making noise.

Reading Data

Usually, a device can be read from as well as written to. Reading /dev/audio usually returns digitized data from a microphone, but I haven't been asked to provide this feature, and I have no real interest in hearing my voice.

When I built my first alpha release of the physical device, I found the need to time the interrupt rate, in order to be sure it was close enough to the expected 8KHz. (In the alpha version, I used a variable resistor to fine-tune the frequency, and I needed a way to check how it went.) The easiest solution that came to mind was to use the clock of the host computer to measure the time lapses.

To this end, I modified the interrupt handler so that it would write timestamps to an input buffer whenever the device was being read. The input buffer is a circular buffer just like the output buffer described above.

The previous excerpt from sad_interrupt showed that after writing an audio sample, the function returns to the caller. Any additional lines, therefore, are only executed if no audio data is there, so the rest of the ISR has thus been devoted to collecting timing information. This shows how I implemented “if there is no pending output, deal with input” rather than the more correct “if something is reading, give it some data.” This is acceptable as long as the device is not meant to be read from and written to at the same time in a production environment.

static struct timeval tv, tv_last;
unsigned long diff;
diff = (tv.tv_sec - tv_last.tv_sec) * 1000000 +
   (tv.tv_usec - tv_last.tv_usec);
tv_last = tv;
/* Write 16 bytes, assume bufsize
 * is a multiple of 16 */
ihead += sprintf((char *)ihead,"%15u\n",
if (ihead == ibuffer + IBUFFER_SIZE)
   ihead = ibuffer; /* wrap */
wake_up_interruptible(&inq); /*
        anyone reading? */

Printing the time difference between two samples has two advantages over printing the absolute time: data is directly meaningful to humans without resorting to external filters, and any overflow of the input buffer will have no effect on the perceived results, other than the loss of a few samples.

Real tests show the reported interrupt rate is not as steady as one would hope. Some system activities require you to disable interrupt reporting, and this introduces some delay in the execution of the ISR routine. Nonetheless, an oscillation of a few microseconds is perfectly acceptable and it is not perceived in the resulting audio, which is not high-fidelity anyway.

It's interesting to note that disk activity can introduce some real distortion in the audio stream, since servicing an IDE interrupt can take as long as two milliseconds (on my system). The IDE driver disables interrupt reporting while its own ISR is active, and the huge delay results in eight lost interrupts from the parallel port, which in turn causes a noticeable distortion of the audio data stream.

If you read from sad during disk activity, you'll see the long time intervals; writing to the device produces very bad audio. The easy solution to this problem is invoking

/sbin/hdparm -u 1 /dev/hda

before playing any audio. The command tells the disk drive not to disable reporting interrupts while it is servicing its own. Refer to the hdparm documentation to probe further.