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

284
kernel/dev/audio.c Normal file
View file

@ -0,0 +1,284 @@
// AudioEngine: the audio class, and /dev/dsp above it.
//
// See include/audio.h for what this is and what it deliberately is not. In
// short: a program opens /dev/dsp, says what its samples are, writes them, and
// stops. Everything here is either the class vtable's thin wrapper or the part
// of "play this" that is not specific to a chip.
#include "types.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "sleeplock.h"
#include "fs.h"
#include "file.h"
#include "device.h"
#include "audio.h"
#include "proc.h"
#include "uaccess.h"
#include "errno.h"
static struct spinlock audio_lock;
static int audio_lock_init;
// The format the device is currently in. Kept here rather than asked of the
// driver on every write, because a caller that set 44100 and got 48000 needs
// to be told once and then have it stay told.
static struct audio_format current_fmt = { 48000, 2, AUDIO_FMT_S16LE };
static void
lock_once(void)
{
if (!audio_lock_init) {
initlock(&audio_lock, "audio");
audio_lock_init = 1;
}
}
// --- the class -------------------------------------------------------------
struct device *
audio_get(void)
{
return device_find_class(DEVCLASS_AUDIO);
}
static const struct audio_ops *
ops_of(struct device *d)
{
return d && d->ops ? (const struct audio_ops *)d->ops : 0;
}
int
audio_configure(struct audio_format *f)
{
struct device *d = audio_get();
const struct audio_ops *o = ops_of(d);
int r;
if (!o || !o->configure)
return -ENODEV;
if (f->bits != AUDIO_FMT_S16LE)
return -EINVAL;
if (f->channels != 1 && f->channels != 2)
return -EINVAL;
if (f->rate < 4000 || f->rate > 192000)
return -EINVAL;
r = o->configure(d, f);
if (r < 0)
return r;
lock_once();
acquire(&audio_lock);
current_fmt = *f; // what the driver settled on, not what was asked
release(&audio_lock);
return 0;
}
// Queue bytes, blocking while the hardware is full.
//
// The blocking is here rather than in a driver because it is the same in every
// driver: there is a ring, it has room or it does not, and a writer that is
// ahead of the speaker has nothing useful to do but wait. Sleeping on the
// device and being woken by its interrupt is the whole of it.
int
audio_write(const void *pcm, int bytes)
{
struct device *d = audio_get();
const struct audio_ops *o = ops_of(d);
const char *p = pcm;
int done = 0;
if (!o || !o->queue)
return -ENODEV;
if (bytes <= 0)
return 0;
while (done < bytes) {
int n = o->queue(d, p + done, bytes - done);
if (n < 0)
return done ? done : n;
if (n == 0) {
// Full. Start what is already queued -- a writer that fills the ring
// and then waits for space would otherwise wait for a stream nobody
// started -- and give the CPU up until some of it has been played.
if (o->start)
o->start(d);
if (myproc() && myproc()->killed)
return done ? done : -EINTR;
audio_wait(d);
continue;
}
done += n;
}
if (o->start)
o->start(d);
return done;
}
// Wait for the hardware to make room. A driver signals this from its
// interrupt handler with audio_wakeup().
//
// The timeout is not a nicety: a driver whose interrupt never arrives -- a
// misrouted line, a chip that needs a kick -- would otherwise hang the writer
// for ever with no way to tell what happened. Waking up anyway and asking the
// hardware again turns that into audio that stutters, which is a bug report
// somebody can make.
void
audio_wait(struct device *d)
{
uint t0;
(void)d;
// One timer tick, the same way kernel/sys/sys_poll.c waits: there is no
// timed sleep() in this kernel, and adding one for a single caller would be
// a scheduler change to avoid a ten-line loop.
//
// Sleeping on `ticks` rather than on the device means a driver interrupt is
// not strictly required to make progress -- which is the point. A ring that
// drains is a ring that has room, and asking again in ten milliseconds
// costs one wakeup per buffer.
acquire(&tickslock);
t0 = ticks;
while (ticks == t0 && !(myproc() && myproc()->killed))
sleep(&ticks, &tickslock);
release(&tickslock);
}
void
audio_drain(void)
{
struct device *d = audio_get();
const struct audio_ops *o = ops_of(d);
int spins;
if (!o || !o->pending)
return;
// Bounded, for the same reason the wait above is. Ten seconds of audio is
// longer than any buffer this engine will hold, so a `pending` that has not
// reached zero by then is a driver that is not playing rather than one that
// is busy.
for (spins = 0; spins < 1000 && o->pending(d) > 0; spins++) {
if (myproc() && myproc()->killed)
return;
audio_wait(d);
}
}
void
audio_stop(void)
{
struct device *d = audio_get();
const struct audio_ops *o = ops_of(d);
if (o && o->stop)
o->stop(d);
}
// --- /dev/dsp ---------------------------------------------------------------
int
dspwrite(struct inode *ip, uint64 off, char *src, int n)
{
(void)ip; (void)off;
return audio_write(src, n);
}
// Recording is not implemented, and says so. Returning zero would look like
// end-of-file and a recorder would write an empty file and call it a success.
int
dspread(struct inode *ip, uint64 off, char *dst, int n)
{
(void)ip; (void)off; (void)dst; (void)n;
return -ENOSYS;
}
// The last descriptor is closing. Anything still queued is played out first:
// a program that writes a sound and exits expects to hear the end of it, and
// dropping the tail is the one behaviour that would make this device feel
// broken while technically working.
void
dspclose(struct inode *ip)
{
(void)ip;
audio_drain();
audio_stop();
}
int
dsp_ioctl(int request, uint64 argp)
{
struct device *d = audio_get();
const struct audio_ops *o = ops_of(d);
struct audio_format f;
int val;
if (!o)
return -ENODEV;
lock_once();
acquire(&audio_lock);
f = current_fmt;
release(&audio_lock);
switch (request) {
case SNDCTL_DSP_SPEED:
case SNDCTL_DSP_CHANNELS:
case SNDCTL_DSP_SETFMT: {
if (copy_from_user(&val, argp, sizeof(val)) < 0)
return -EFAULT;
if (request == SNDCTL_DSP_SPEED) f.rate = (unsigned)val;
else if (request == SNDCTL_DSP_CHANNELS) f.channels = (unsigned)val;
else {
if (val != AFMT_S16_LE)
return -EINVAL;
f.bits = AUDIO_FMT_S16LE;
}
int r = audio_configure(&f);
if (r < 0)
return r;
// OSS writes back what was actually set, which is the entire reason these
// are read/write ioctls: a program asks for 44100, is told 48000, and
// resamples. One that ignores the answer gets a chipmunk, and that is its
// bug rather than ours.
val = (request == SNDCTL_DSP_SPEED) ? (int)f.rate
: (request == SNDCTL_DSP_CHANNELS) ? (int)f.channels
: AFMT_S16_LE;
if (copy_to_user(argp, &val, sizeof(val)) < 0)
return -EFAULT;
return 0;
}
case SNDCTL_DSP_SYNC:
audio_drain();
return 0;
case SNDCTL_DSP_RESET:
audio_stop();
return 0;
case SNDCTL_DSP_GETOSPACE: {
struct audio_buf_info bi;
int space = o->space ? o->space(d) : 0;
bi.fragsize = 4096;
bi.bytes = space;
bi.fragments = space / bi.fragsize;
bi.fragstotal = 0;
if (copy_to_user(argp, &bi, sizeof(bi)) < 0)
return -EFAULT;
return 0;
}
}
return -ENOTTY;
}

View file

@ -211,4 +211,8 @@ struct devsw devsw[] = {
// reverse of major 9: that hands a device to a process, this lets a process
// be one. See include/netuser.h.
[11] = { netuser_read, netuser_write, netuser_ready },
// /dev/dsp (major 12) -- UNIX audio, kernel/dev/audio.c. Write PCM to it.
// The close hook plays out what is still queued: a program that writes a
// sound and exits expects to hear the end of it.
[12] = { dspread, dspwrite, 0, dspclose },
};

View file

@ -40,6 +40,7 @@ static const char *class_names[DEVCLASS_NCLASS] = {
[DEVCLASS_STORAGE] = "storage",
[DEVCLASS_NET] = "net",
[DEVCLASS_BUS] = "bus",
[DEVCLASS_AUDIO] = "audio",
};
const char *

View file

@ -365,6 +365,7 @@ pci_class_of(const struct pci_dev *d)
case PCI_CLASS_STORAGE: return DEVCLASS_STORAGE;
case PCI_CLASS_NETWORK: return DEVCLASS_NET;
case PCI_CLASS_SERIAL_BUS: return DEVCLASS_BUS;
case PCI_CLASS_MULTIMEDIA: return DEVCLASS_AUDIO;
default: return DEVCLASS_OTHER;
}
}