SchOS/docs/architecture/audio.md
Nexoniarz 26bca020b6 licence: SchOS is GPL-2.0-or-later, no longer Apache-2.0
Nexoniarz is the sole author and SchOS was never published under
Apache-2.0, so this is a straight relicence. LICENSE is the FSF GPLv2 text,
every first-party SPDX header moves to GPL-2.0-or-later, and NOTICE,
README.md, docs/licensing.md and the site say so. projects/schfs-linux
follows the rest of the tree.

The GPL gives copyleft: a fork has to ship its source. It also lifts the
old "Linux drivers, never" rule, since GPL-2.0-only code combines with
GPL-2.0-or-later. "Or later" leaves room for GPLv3, but one binary still
has to pick one version, so docs/licensing.md now keeps Apache-2.0 and
GPLv3 code out of the kernel to keep Linux code possible there. BSD
4-clause code goes from "avoid" to "no", because the advertising clause
is GPL-incompatible.

LDLS still ships no .ko. That boundary is now project policy rather than
a legal requirement, and ldls.md, devices.md and licensing.md section 6
say so. refmod declares license=GPL, and ldls-check passes (27 checks).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:48:45 +02:00

5.7 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 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.

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.

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/dsp returns ENOSYS rather 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.