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:
parent
3eb2e97f7b
commit
9fea18b18b
18 changed files with 1110 additions and 5 deletions
103
include/audio.h
Normal file
103
include/audio.h
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
#pragma once
|
||||
|
||||
// The audio class, and the engine above it.
|
||||
//
|
||||
// A program that wants to make a sound should be able to open a device, write
|
||||
// samples to it, and stop -- which is what UNIX audio has always been, and
|
||||
// what this is. `/dev/dsp`, PCM in, `write()`, and a handful of ioctls to say
|
||||
// what the samples are. No server, no session bus, no protocol.
|
||||
//
|
||||
// The shape is the display class's, for the same reason: a consumer asks for
|
||||
// "the audio device" and gets whatever is bound, so adding a second driver is
|
||||
// not an edit to every caller. Found with device_find_class(DEVCLASS_AUDIO).
|
||||
//
|
||||
// ---------------------------------------------------------------------------
|
||||
//
|
||||
// **What the engine is for, and what it is not yet.**
|
||||
//
|
||||
// AudioEngine owns everything above a driver that is not specific to a chip:
|
||||
// what format the stream is in, how many bytes the hardware can take, and what
|
||||
// a write() does when it cannot take them all. That is genuinely shared -- a
|
||||
// second driver should not re-implement "block until there is room".
|
||||
//
|
||||
// It is deliberately *one stream*. Mixing several programs into one device is
|
||||
// a real feature with a real design behind it, and building it before anything
|
||||
// plays a single sound would be the mistake docs/plan/1.0.md warns about: 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.
|
||||
|
||||
struct device;
|
||||
|
||||
// Interleaved signed 16-bit little-endian, which is what every PC audio chip
|
||||
// takes natively and what a WAV file already contains. Others exist and none
|
||||
// of them is needed to make a sound; converting in the kernel to satisfy a
|
||||
// caller that could have converted first is not an obvious trade.
|
||||
#define AUDIO_FMT_S16LE 16
|
||||
|
||||
struct audio_format {
|
||||
unsigned int rate; // frames per second -- 48000, 44100
|
||||
unsigned int channels; // 1 mono, 2 stereo
|
||||
unsigned int bits; // AUDIO_FMT_S16LE
|
||||
};
|
||||
|
||||
struct audio_ops {
|
||||
// Take this format, or report what the hardware will do instead by writing
|
||||
// back into `f` and returning 0. Returning negative means it cannot play at
|
||||
// all. A driver that only does 48 kHz stereo says so here rather than
|
||||
// failing a write() later, which is the difference between a program that
|
||||
// adapts and a program that gets silence.
|
||||
int (*configure)(struct device *, struct audio_format *f);
|
||||
|
||||
// Bytes that can be queued right now without waiting. The engine uses this
|
||||
// to decide whether a write() has to block, so a driver that returns a
|
||||
// hardware-accurate number is a driver whose write() latency is honest.
|
||||
int (*space)(struct device *);
|
||||
|
||||
// Queue PCM. Returns bytes accepted, which may be fewer than offered and
|
||||
// may be zero; the engine handles the remainder. Never blocks.
|
||||
int (*queue)(struct device *, const void *pcm, int bytes);
|
||||
|
||||
// Start the stream if it is not running. Called by the engine once there is
|
||||
// something to play, rather than at open: a device left running with an
|
||||
// empty ring plays whatever was in it, which on real hardware is a loop of
|
||||
// the last buffer and sounds exactly as bad as it reads.
|
||||
void (*start)(struct device *);
|
||||
|
||||
// Stop, and discard anything queued.
|
||||
void (*stop)(struct device *);
|
||||
|
||||
// Bytes still to be played. Zero means the hardware has finished. The
|
||||
// engine polls this for SNDCTL_DSP_SYNC rather than a driver growing its
|
||||
// own wait loop.
|
||||
int (*pending)(struct device *);
|
||||
};
|
||||
|
||||
// --- what the kernel uses --------------------------------------------------
|
||||
|
||||
struct device *audio_get(void);
|
||||
int audio_configure(struct audio_format *f); // 0, or negative
|
||||
int audio_write(const void *pcm, int bytes); // bytes queued, or negative
|
||||
void audio_drain(void);
|
||||
void audio_stop(void);
|
||||
|
||||
// --- what userspace uses ---------------------------------------------------
|
||||
//
|
||||
// OSS numbering, which is the one every UNIX audio program already knows and
|
||||
// the only reason to invent a different set would be to make ports harder.
|
||||
// These four are the whole interface a tone generator or a WAV player needs.
|
||||
|
||||
#define SNDCTL_DSP_SPEED 0xC0045002 // int in/out: sample rate
|
||||
#define SNDCTL_DSP_CHANNELS 0xC0045006 // int in/out: 1 or 2
|
||||
#define SNDCTL_DSP_SETFMT 0xC0045005 // int in/out: AFMT_S16_LE
|
||||
#define SNDCTL_DSP_SYNC 0x00005001 // wait for the queue to drain
|
||||
#define SNDCTL_DSP_GETOSPACE 0x8010500C // struct audio_buf_info out
|
||||
#define SNDCTL_DSP_RESET 0x00005000 // stop, discard
|
||||
|
||||
#define AFMT_S16_LE 0x00000010
|
||||
|
||||
struct audio_buf_info {
|
||||
int fragments; // how many whole fragments are free
|
||||
int fragstotal; // how many there are in total
|
||||
int fragsize; // bytes in one
|
||||
int bytes; // free bytes, which is the number that matters
|
||||
};
|
||||
|
|
@ -611,6 +611,13 @@ void fbclose(struct inode*);
|
|||
// further syscalls, so it is the last the kernel hears about it.
|
||||
void fb_mapped(void);
|
||||
|
||||
// AudioEngine and /dev/dsp. kernel/dev/audio.c, include/audio.h.
|
||||
int dspread(struct inode*, uint64, char*, int);
|
||||
int dspwrite(struct inode*, uint64, char*, int);
|
||||
void dspclose(struct inode*);
|
||||
int dsp_ioctl(int request, uint64 argp);
|
||||
void audio_wait(struct device *);
|
||||
|
||||
// The crash screen, on displays other than the console's. kernel/dev/device.c.
|
||||
void display_panic_present(void);
|
||||
// Push the console's newly-drawn rows to a second display, if the kernel
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ enum dev_class {
|
|||
DEVCLASS_STORAGE,
|
||||
DEVCLASS_NET,
|
||||
DEVCLASS_BUS, // a controller that finds more devices (xHCI, hubs)
|
||||
DEVCLASS_AUDIO,
|
||||
DEVCLASS_NCLASS
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -187,3 +187,5 @@ extern struct devsw devsw[];
|
|||
#define CONSOLE 1
|
||||
// Major number of /dev/input/eventN. The minor is the device index.
|
||||
#define INPUT_MAJOR 7
|
||||
// Major number of /dev/dsp. See include/audio.h.
|
||||
#define AUDIO_MAJOR 12
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
#define NOFILE 128 // open files per process
|
||||
#define NFILE 512 // open files per system
|
||||
#define NINODE 200 // maximum number of active i-nodes
|
||||
#define NDEV 12 // maximum major device number
|
||||
#define NDEV 13 // maximum major device number
|
||||
#define RAMDISK_DEV 2 // memory-backed block device (kernel/dev/ramdisk.c)
|
||||
#define USBDISK_DEV 3 // USB mass storage (kernel/drivers/usb_storage.c)
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,17 @@ outb(ushort port, uchar data)
|
|||
asm volatile("out %0,%1" : : "a" (data), "d" (port));
|
||||
}
|
||||
|
||||
// 16 bits. Added for AC'97, whose mixer is entirely 16-bit registers: reading
|
||||
// one of them a byte at a time returns the low half twice on some codecs,
|
||||
// because the read of the low byte is what latches the pair.
|
||||
static inline ushort
|
||||
inw(ushort port)
|
||||
{
|
||||
ushort data;
|
||||
asm volatile("inw %1,%0" : "=a" (data) : "d" (port));
|
||||
return data;
|
||||
}
|
||||
|
||||
static inline uint
|
||||
inl(ushort port)
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue