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.

I'm a strange guy, and I want my computers to keep silent—that's why I wrote the “Visible-bell mini-howto”, where I suggest speakerectomy surgery be performed. On the other hand, I enjoy playing with the soldering iron to build irrelevant stuff. One of the most irrelevant things I ever conceived is recycling the computer's loudspeaker in a very-low-volume audio device. As you might imagine, the device plugs in the parallel port.

This article describes the driver for such a beast, shows interesting details of the kernel workings and is still short enough to be an easy text for almost any reader. A quick description of the hardware is mandatory, but you can safely skip over the first section and jump directly to the section called “Writing Data”.

The software described here, as well as the electrical drawing, is released according to the GPL and is available as sad-1.0.tar.gz (Standalone Audio Device) from ftp://ftp.systemy.it/pub/develop/, my own ftp site.

Part of this work has been sponsored by “SAD Trasporto Locale” (http://www.sad.it/), the bus company of Bolzano (Bozen), Italy. They plan to bring my hardware on their buses and renamed the company to match my package (smile). (See “Travelling Linux” by Maurizio Cachia, LJ, June 1997.)

Figure 1. Audio Device Schematic

The Underlying Hardware

My device plugs in the parallel port, and its schematics are depicted in Figure 1; The photograph under the tiele is the only model ever built (Italian buses will run a different flavour of such stuff, the “bus for bus”--ftp://ftp.systemy.it/pub/develop/b4b-X.YY.tar.gz).

I owe the basic idea to Michael Beck, author of the pcsndrv package; the idea sounds like “use the parallel data bits to output audio samples.” My own addition is “use the interrupt signal to strobe samples at the right pace.” Audio samples must flow at 8KHz and any not-so-ancient computer can sustain such an interrupt rate: my almost-ancient development box runs a 33 BogoMips processor and is perfectly happy playing parallel audio. The interrupt-based approach trades higher quality for increased hardware complexity than that needed by Michael's package.

As shown in the schematics, the device is made up of a simple D/A converter built with a few resistors; the signal is then reduced to 1.5V peak-to-peak amplitude and fed through a low-pass filter. The filter I chose is a switched-capacitor device driven by a square wave at ten times the cutoff frequency. The 6142 chip is a dual op-amp with rail-to-rail output, one of several possible choices for low-power single-supply equipment.

The output signal can be brought to a small loudspeaker, but can be listened to only in complete silence; other environments ask for some form of amplification. My preferred alternative to the amplifier is the oscilloscope, the typical hear-by-seeing approach.

Writing Data

The main role of an audio driver is pushing data through the audio device. Several kinds of audio devices exist, and the sad driver only implements the /dev/audio flavour: 8-bit samples flowing at a rate of 8KHz. Each data byte that gets written to /dev/audio should be fed to an 8-bit A/D converter; every 125 microseconds, a new data sample must replace the current one.

Timing issues should be managed by the driver, without intervention from the program writing out the audio data. The output buffer is the software tool that isolates timing issues from user programs.

In sad, the output buffer is allocated at load time using get_free_pages. This function allocates consecutive pages, a power of two of them; the order argument of the function specifies how many pages are requested and is used as a power of two. An order of 1, therefore, represents two pages and an order of 3 represents eight pages. The allocation order of the output buffer is stored in the macro OBUFFER_ORDER, which is 0 in the distributed source file. This accounts for one page, which on the x86 processor corresponds to 4KB, or half a second worth of data.

The output buffer of sad is a circular buffer; the pointers ohead and otail represent its starting and ending points. The kernel uses unsigned long values to represent physical addresses, and the same convention is used in sad:

static unsigned long obuffer = 0;
static unsigned long volatile ohead, otail;

Note that the ohead and otail variables are declared as volatile to prevent the compiler from caching their value in processor registers. This is an important caution, as the variables will be modified at interrupt time, asynchronously with respect to the rest of the code.

We'll see later that sad has an input buffer as well; the overall buffer allocation consists of these lines, executed from within init_module:

obuffer = __get_free_pages(GFP_KERNEL,
   OBUFFER_ORDER, 0 /* no dma */);
ohead = otail = obuffer;
ibuffer = __get_free_pages(GFP_KERNEL,
   IBUFFER_ORDER, 0 /* no dma */);
ihead = itail = ibuffer;
if (!ibuffer || !obuffer) { /* allocation failed
cleanup_module(); /* use your own function */
return -ENOMEM;

Any data that a process writes to the device is put in the circular buffer, as long as it fits. When the buffer is full, the writing process is put to sleep, waiting for some space to be freed.

Since the data samples flow out smoothly, the process will eventually be awakened to complete its write system call. Anyway, a good driver is prepared to deal with users hitting the ctrl-C and must deal with SIGINT and other signals.

The following lines are needed to put to sleep and awaken the current process, all the magic is hidden in interruptible_sleep_on:

   if (current->signal & ~current->blocked)
       /* tell the fs layer to handle it */
      /* a signal arrived */
      return -ERESTARTSYS;
/* else, loop */
/* the following code writes to
 * the circular buffer */

What are OBUFFER_FREE and OBUFFER_THRESHOLD? They are two macros: the former accesses ohead and otail to find out how much free space is in the buffer; the latter is a simple constant, predefined to 1024, a pseudo-random number. The role of such a threshold is to preserve system resources by avoiding too frequent asleep->awake transitions.

If the threshold was 1, the process would need to be awakened as soon as one byte of the buffer was freed, but it would soon be put to sleep again. As a result, the process will always be running, consuming processing power and raising the machine load. A threshold of 1KB assures that when the process goes to sleep it will sleep for at least one tenth of a second, because it won't be awakened before 1KB of data flows through the audio device. You can recompile sad.c with a different threshold value to see how a small value keeps the processor busy. Too big a value can result in jumpy audio, i.e. the sound cuts in and out. The audio stream becomes jumpy because data continues to flow while the kernel schedules execution of the process writing audio data. The more heavily the computer is loaded, the more jumpy the audio is likely to be; if several processes are contending for the processor, the one playing audio might be awakened too late, after all pending data has been transferred to the audio device. In addition to lowering the wakeup threshold, you can also cure the problem by increasing the buffer size.

Naturally, the write device method is only half of the story; the other half is performed by the interrupt handler.


Geek Guide
The DevOps Toolbox

Tools and Technologies for Scale and Reliability
by Linux Journal Editor Bill Childers

Get your free copy today

Sponsored by IBM

Upcoming Webinar
8 Signs You're Beyond Cron

Scheduling Crontabs With an Enterprise Scheduler
11am CDT, April 29th
Moderated by Linux Journal Contributor Mike Diehl

Sign up now

Sponsored by Skybot