kernel (threads, demand paging, TLB shootdowns, dynamic linking, RAM direct map), syscalls, filesystem (/proc, /sys, /dev), terminals (12, VT handshake, ptys), networking (r8169 through LDLS), LDLS (module sets; Linux code may ship), devices (what stays in the kernel; hybrid.md's lasting content), boot (the loader's placement of the kernel), graphics (fbdev, GMMOS, QR 1-40), audio (ALSA over OSS), debugging, usb. New: packages.md (the apk root) and desktop.md (Xorg on the framebuffer). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EVRhxi9zpdW2uDUN4FVcao
6.8 KiB
Audio
A program that wants to make a sound opens /dev/dsp, writes PCM to it, and
closes it. That is what UNIX audio has always been, and it is what this is.
tone 440 2 # 440 Hz for two seconds
Three pieces, in the order a sample passes through them:
/dev/dsp |
kernel/dev/audio.c. A character device: write() is audio, and a handful of OSS ioctls say what the samples are. |
| AudioEngine | The same file. 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 | kernel/drivers/audio/ac97.c today. Bound by class, exactly as a display driver is — device_find_class(DEVCLASS_AUDIO). |
Why OSS and not something new
The ioctls are SNDCTL_DSP_SPEED, SNDCTL_DSP_CHANNELS, SNDCTL_DSP_SETFMT,
SNDCTL_DSP_SYNC and SNDCTL_DSP_GETOSPACE, with OSS's own numbering. They
are not invented here and they are not copied for nostalgia: every UNIX audio
program that has ever been written knows them, 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 everything 9% fast, and that is its bug
rather than the driver's. configure in the class works the same way, and for
the same reason — a driver that can only do 48 kHz says so there, rather than
by quietly failing a write() later.
What the engine is, and what it deliberately is not
It owns format negotiation, the "is there room" question, and blocking. Those are genuinely shared: a second driver should not have to re-implement wait until the speaker has caught up.
It is one stream. Mixing several programs into one device is a real feature with a real design behind it, and building it before anything had played a single sound would be exactly the mistake ../plan/1.0.md keeps a register against: 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.
Waiting on the tick, not on the device
audio_wait() sleeps until the next timer tick rather than on an interrupt
from the sound card, which looks like the lazy choice and is not.
A ring that drains is a ring with room in it, so asking again in ten milliseconds always makes progress. Sleeping on the device would make audio depend on the interrupt arriving — and on a machine whose line is misrouted, or a chip that needs a kick, that is not a stutter, it is a writer that never wakes up. The interrupt handler is still there and still accounts for finished buffers; the point is that deleting it would cost efficiency and not correctness.
AC'97 first, and HDA later
AC'97 is the simplest audio device that exists both on real machines and in QEMU. Intel HDA is what a laptop built this century actually has, and it is a much larger driver — codec enumeration, CORB/RIRB, stream descriptors. Writing it first would have meant writing two unproven things at once: a driver, and the interface it is the only user of.
The class is the whole point of the order. HDA arrives as a second
struct audio_ops and nothing above it changes.
How it plays
The chip reads a buffer descriptor list: 32 entries, each naming a physical
address and a length. It plays the entry at CIV and stops when it reaches
LVI, the last valid index. Playing continuously is therefore not "start it
and walk away" — it is keeping LVI ahead of CIV, which is what queue()
does, one 4 KB descriptor at a time.
Retiring finished descriptors happens in both queue() and the interrupt
handler, through the same tail pointer so the two cannot disagree. That is
the same "the interrupt is an optimisation" stance as above.
Both register files are I/O ports, not MMIO — which is why nothing in the
driver maps anything. bar[n].is_io is what says so, and a driver that
ioremaped these would be mapping a port number as though it were an address.
Through ALSA, for programs that expect it
SDL 3 has no OSS backend and SchOS has no ALSA kernel side, so sound from SDL games
and anything else that speaks ALSA goes SDL → alsa-lib → alsa-plugins' OSS plugin →
/dev/dsp. alsa-lib and alsa-plugins are in the Alpine world, and
/etc/asound.conf (from schos-base) makes the default device a plug onto the OSS
device at 16-bit stereo 48 kHz, which is what every SchOS audio driver plays.
Running that path found two bugs underneath it:
- AC'97: resetting the PCM-out box also clears the descriptor-list address and nothing set it again, so every stream after the first stop played a "list" at physical address 0 — silence — and ALSA resets before each stream. If the engine halts at the end of the queue, the run bit is set again.
/dev/dspwas missing the ioctls the plugin's ring-and-pointer model uses:SETFRAGMENT,GETFMTS,GETCAPS,SETTRIGGER,GETOPTR,GETODELAY, andpoll(), writable once a whole fragment fits (ALSA spun at 76% CPU otherwise).speaker-testnow plays in real time at 0% CPU, verified in a recording of QEMU's AC'97.
How it is tested
QEMU's wav audio backend writes what the guest plays to a file, so the test
is not "it did not crash":
make qemu # QEMU_AUDIO puts an AC97 on every target
tone 440 2
and the file is measured — 1.97 seconds of 44.1 kHz stereo, peak amplitude 14177 against the tone generator's table maximum of 14178, and 880 zero crossings per second, which is 440 Hz exactly.
wav rather than a host backend on purpose: a build machine may have no sound
card, no PulseAudio and no session bus, and it is the only backend whose output
can be checked. To actually hear it:
make qemu QEMU_AUDIO='-audiodev pa,id=snd0 -device AC97,audiodev=snd0'
What it does not do
- Recording.
read()on/dev/dspreturnsENOSYSrather than zero, because zero is end-of-file and a recorder would write an empty file and call it a success. - Mixing. One stream; see above.
- Mono. The driver reports two channels because AC'97 plays stereo, and a mono stream would have to be doubled somewhere. The write-back tells the caller, which is the whole reason the ioctl has a direction.
- Formats other than signed 16-bit little-endian. It is what every PC audio chip takes natively and what a WAV file already contains. Converting in the kernel to satisfy a caller that could have converted first is not an obvious trade.
- Volume. The mixer is set to full and left there. How loud a machine is is not the kernel's decision, and a device that comes up silent reads as broken to whoever is testing it.