audio: /dev/dsp, AudioEngine, and an AC'97 under it

A program that wants to make a sound opens /dev/dsp, writes PCM, and closes
it. That is what UNIX audio has always been and it now exists here:

    tone 440 2

Three pieces, in the order a sample passes through them. **/dev/dsp** is a
character device where write() is audio and a handful of OSS ioctls say what
the samples are. **AudioEngine** is everything above a driver that is not
specific to a chip -- the format, whether there is room, and what a write does
when there is not. **A driver** is bound by class, exactly as a display driver
is: device_find_class(DEVCLASS_AUDIO).

OSS numbering rather than something new, because every UNIX audio program ever
written already knows it and the only thing a different set would achieve is
making ports harder. They are read/write ioctls, which is the part that
matters: a program asks for 44100, the device sets 48000 and writes 48000
back, the program resamples. One that ignores the answer plays 9% fast, and
that is its bug rather than the driver's.

**The engine is deliberately one stream.** Mixing several programs is a real
feature with a real design behind it, and building it before anything had
played a single sound would be exactly what docs/plan/1.0.md keeps a register
for: work that exists and is connected to nothing. The second program that
wants to play at the same time is what should cause it to be written.

**audio_wait() sleeps on the timer tick, not on the device.** That looks like
the lazy choice and is not: a ring that drains is a ring with room, so asking
again in ten milliseconds always makes progress, whereas sleeping on the card
makes audio depend on an interrupt arriving. On a machine whose line is
misrouted that is not a stutter, it is a writer that never wakes. The handler
still accounts for finished buffers -- deleting it would cost efficiency, not
correctness -- and it retires descriptors through the same `tail` pointer
queue() uses, so the two cannot disagree about what is in flight.

**AC'97 first because HDA is two unproven things at once.** It is the simplest
audio device that exists on real machines and in QEMU both. Intel HDA is what
the laptop actually has and is a much larger driver; writing it against an
interface nothing had ever played through would have meant debugging the
driver and the interface simultaneously. HDA arrives as a second
struct audio_ops and nothing above it changes -- which is the entire point of
the class.

Two things the wiring needed: PCI class 0x04 now maps to DEVCLASS_AUDIO (the
driver bound and /proc/devices showed it, but audio_get() found nothing,
because the class byte had never been translated), and include/x86.h grew
inw() -- AC'97's mixer is all 16-bit registers and reading one a byte at a
time returns the low half twice on some codecs.

**Tested by measuring what came out, not by not crashing.** QEMU's wav backend
writes the guest's audio to a file; `tone 440 2` produced 1.97 seconds of
44.1 kHz stereo with peak amplitude 14177 against the generator's table
maximum of 14178, and 880 zero crossings per second -- 440 Hz exactly. wav is
on every QEMU target rather than a host backend, because a build machine may
have no sound card and it is the only backend whose output can be checked.

Both supported configurations: one boot each, no traps, no panics, socktest
and memfdtest green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-09-24 14:23:13 +02:00
commit 9fea18b18b
18 changed files with 1110 additions and 5 deletions

134
cmd/tone/main.c Normal file
View file

@ -0,0 +1,134 @@
// tone -- write a sine wave to /dev/dsp.
//
// The smallest program that proves the audio path end to end: it asks the
// device what it can play, generates that, and writes it. If a machine makes
// this noise, every layer between a write() and a speaker is working.
//
// tone 440 Hz for one second
// tone 1000 1000 Hz for one second
// tone 1000 3 1000 Hz for three seconds
//
// Deliberately not a WAV player. A player has to parse a header, and a header
// is a second thing to get wrong while testing the first.
#include "types.h"
#include "user.h"
#include "fcntl.h"
#include "audio.h"
// A quarter-amplitude sine, from a table rather than a libm that does not
// exist here. 64 points is audibly a sine and arithmetically exact.
static const short SINE[64] = {
0, 803, 1605, 2404, 3196, 3980, 4753, 5511,
6254, 6977, 7679, 8357, 9008, 9630, 10220, 10777,
11297, 11780, 12222, 12623, 12980, 13293, 13559, 13778,
13951, 14075, 14151, 14178, 14156, 14085, 13965, 13797,
13581, 13318, 13009, 12655, 12257, 11818, 11338, 10820,
10266, 9679, 9060, 8413, 7740, 7044, 6329, 5597,
4851, 4096, 3334, 2569, 1804, 1043, 289, -453,
-1180, -1889, -2576, -3239, -3874, -4479, -5051, -5588
};
// lib/ has no stdio: write(2) is the whole of it, which is why every
// first-party program in cmd/ carries these three lines.
static void
say(const char *s)
{
int n = 0;
while (s[n]) n++;
write(1, (void *)s, n);
}
static void
sayn(int v)
{
char b[12];
int i = 0, j;
if (v == 0) { say("0"); return; }
if (v < 0) { say("-"); v = -v; }
while (v && i < 11) { b[i++] = (char)('0' + v % 10); v /= 10; }
for (j = i; j-- > 0; )
write(1, &b[j], 1);
}
static int
num(const char *s)
{
int v = 0;
while (*s >= '0' && *s <= '9')
v = v * 10 + (*s++ - '0');
return v;
}
int
main(int argc, char *argv[])
{
int hz = argc > 1 ? num(argv[1]) : 440;
int secs = argc > 2 ? num(argv[2]) : 1;
int rate = 48000, channels = 2, fmt = AFMT_S16_LE;
short buf[1024]; /* 512 stereo frames */
int fd, phase = 0;
if (hz < 20 || hz > 20000) {
say("tone: "); sayn(hz); say(" Hz is not audible\n");
return 1;
}
if (secs < 1 || secs > 30)
secs = 1;
fd = open("/dev/dsp", O_WRONLY);
if (fd < 0) {
say("tone: no /dev/dsp -- is there an audio device?\n");
return 1;
}
// Ask, then believe the answer. These are read/write on purpose: the
// device writes back what it actually set, and a program that ignores
// that plays at the wrong speed and blames the driver.
if (ioctl(fd, SNDCTL_DSP_SETFMT, &fmt) < 0 || fmt != AFMT_S16_LE) {
say("tone: the device will not take 16-bit samples\n");
return 1;
}
ioctl(fd, SNDCTL_DSP_CHANNELS, &channels);
ioctl(fd, SNDCTL_DSP_SPEED, &rate);
say("tone: "); sayn(hz); say(" Hz for "); sayn(secs);
say(" s at "); sayn(rate); say(" Hz, "); sayn(channels);
say(" channel(s)\n");
// The phase step in 1/256ths of a table entry, so a 440 Hz tone at 48 kHz
// is not quantised into something a tuner would argue with.
int step = (hz * 64 * 256) / rate;
int frames_total = rate * secs;
while (frames_total > 0) {
int frames = frames_total > 512 ? 512 : frames_total;
int i;
for (i = 0; i < frames; i++) {
short s = SINE[(phase >> 8) & 63];
phase += step;
if (channels == 2) {
buf[i * 2] = s;
buf[i * 2 + 1] = s;
} else {
buf[i] = s;
}
}
int bytes = frames * channels * 2;
if (write(fd, buf, bytes) != bytes) {
say("tone: short write -- the device stopped taking audio\n");
close(fd);
return 1;
}
frames_total -= frames;
}
// Wait for it to finish rather than exiting on top of it. close() drains
// too, but saying so is the point of the ioctl existing.
ioctl(fd, SNDCTL_DSP_SYNC, 0);
close(fd);
return 0;
}