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>
134 lines
4.1 KiB
C
134 lines
4.1 KiB
C
// tone -- write a sine wave to /dev/dsp.
|
|
//
|
|
// The smallest program that proves the audio path end to end: it asks the
|
|
// device what it can play, generates that, and writes it. If a machine makes
|
|
// this noise, every layer between a write() and a speaker is working.
|
|
//
|
|
// tone 440 Hz for one second
|
|
// tone 1000 1000 Hz for one second
|
|
// tone 1000 3 1000 Hz for three seconds
|
|
//
|
|
// Deliberately not a WAV player. A player has to parse a header, and a header
|
|
// is a second thing to get wrong while testing the first.
|
|
|
|
#include "types.h"
|
|
#include "user.h"
|
|
#include "fcntl.h"
|
|
#include "audio.h"
|
|
|
|
// A quarter-amplitude sine, from a table rather than a libm that does not
|
|
// exist here. 64 points is audibly a sine and arithmetically exact.
|
|
static const short SINE[64] = {
|
|
0, 803, 1605, 2404, 3196, 3980, 4753, 5511,
|
|
6254, 6977, 7679, 8357, 9008, 9630, 10220, 10777,
|
|
11297, 11780, 12222, 12623, 12980, 13293, 13559, 13778,
|
|
13951, 14075, 14151, 14178, 14156, 14085, 13965, 13797,
|
|
13581, 13318, 13009, 12655, 12257, 11818, 11338, 10820,
|
|
10266, 9679, 9060, 8413, 7740, 7044, 6329, 5597,
|
|
4851, 4096, 3334, 2569, 1804, 1043, 289, -453,
|
|
-1180, -1889, -2576, -3239, -3874, -4479, -5051, -5588
|
|
};
|
|
|
|
// lib/ has no stdio: write(2) is the whole of it, which is why every
|
|
// first-party program in cmd/ carries these three lines.
|
|
static void
|
|
say(const char *s)
|
|
{
|
|
int n = 0;
|
|
while (s[n]) n++;
|
|
write(1, (void *)s, n);
|
|
}
|
|
|
|
static void
|
|
sayn(int v)
|
|
{
|
|
char b[12];
|
|
int i = 0, j;
|
|
|
|
if (v == 0) { say("0"); return; }
|
|
if (v < 0) { say("-"); v = -v; }
|
|
while (v && i < 11) { b[i++] = (char)('0' + v % 10); v /= 10; }
|
|
for (j = i; j-- > 0; )
|
|
write(1, &b[j], 1);
|
|
}
|
|
|
|
static int
|
|
num(const char *s)
|
|
{
|
|
int v = 0;
|
|
while (*s >= '0' && *s <= '9')
|
|
v = v * 10 + (*s++ - '0');
|
|
return v;
|
|
}
|
|
|
|
int
|
|
main(int argc, char *argv[])
|
|
{
|
|
int hz = argc > 1 ? num(argv[1]) : 440;
|
|
int secs = argc > 2 ? num(argv[2]) : 1;
|
|
int rate = 48000, channels = 2, fmt = AFMT_S16_LE;
|
|
short buf[1024]; /* 512 stereo frames */
|
|
int fd, phase = 0;
|
|
|
|
if (hz < 20 || hz > 20000) {
|
|
say("tone: "); sayn(hz); say(" Hz is not audible\n");
|
|
return 1;
|
|
}
|
|
if (secs < 1 || secs > 30)
|
|
secs = 1;
|
|
|
|
fd = open("/dev/dsp", O_WRONLY);
|
|
if (fd < 0) {
|
|
say("tone: no /dev/dsp -- is there an audio device?\n");
|
|
return 1;
|
|
}
|
|
|
|
// Ask, then believe the answer. These are read/write on purpose: the
|
|
// device writes back what it actually set, and a program that ignores
|
|
// that plays at the wrong speed and blames the driver.
|
|
if (ioctl(fd, SNDCTL_DSP_SETFMT, &fmt) < 0 || fmt != AFMT_S16_LE) {
|
|
say("tone: the device will not take 16-bit samples\n");
|
|
return 1;
|
|
}
|
|
ioctl(fd, SNDCTL_DSP_CHANNELS, &channels);
|
|
ioctl(fd, SNDCTL_DSP_SPEED, &rate);
|
|
|
|
say("tone: "); sayn(hz); say(" Hz for "); sayn(secs);
|
|
say(" s at "); sayn(rate); say(" Hz, "); sayn(channels);
|
|
say(" channel(s)\n");
|
|
|
|
// The phase step in 1/256ths of a table entry, so a 440 Hz tone at 48 kHz
|
|
// is not quantised into something a tuner would argue with.
|
|
int step = (hz * 64 * 256) / rate;
|
|
int frames_total = rate * secs;
|
|
|
|
while (frames_total > 0) {
|
|
int frames = frames_total > 512 ? 512 : frames_total;
|
|
int i;
|
|
|
|
for (i = 0; i < frames; i++) {
|
|
short s = SINE[(phase >> 8) & 63];
|
|
phase += step;
|
|
if (channels == 2) {
|
|
buf[i * 2] = s;
|
|
buf[i * 2 + 1] = s;
|
|
} else {
|
|
buf[i] = s;
|
|
}
|
|
}
|
|
|
|
int bytes = frames * channels * 2;
|
|
if (write(fd, buf, bytes) != bytes) {
|
|
say("tone: short write -- the device stopped taking audio\n");
|
|
close(fd);
|
|
return 1;
|
|
}
|
|
frames_total -= frames;
|
|
}
|
|
|
|
// Wait for it to finish rather than exiting on top of it. close() drains
|
|
// too, but saying so is the point of the ioctl existing.
|
|
ioctl(fd, SNDCTL_DSP_SYNC, 0);
|
|
close(fd);
|
|
return 0;
|
|
}
|