SchOS/include/audio.h
Nexoniarz 9fea18b18b 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>
2026-09-24 14:23:13 +02:00

103 lines
4.7 KiB
C

#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
};